# playX 接口文档（按调用方向拆分）

说明：本文档严格依据当前代码 `app/api/controller/v1/Playx.php`、`app/api/controller/v1/Auth.php`（临时登录）、`config/playx.php` 与定时任务 `app/process/PlayxJobs.php` 整理。

按调用方向分为三类（避免与历史章节标题混淆）：

| 方向 | 含义 | 本文位置 |
|------|------|----------|
| **playX → 积分商城** | playX（或上游批处理）**主动 HTTP 调用商城**开放接口 | **§1**（如 Daily Push） |
| **积分商城 → playX** | 商城 Worker / 后台 **主动 HTTP 调用 playX / Cash Market** 提供的接口 | **不展开于本文**；交付 playX 的说明见 **`docs/playX-接口待完善清单.md`** |
| **积分商城 → H5** | H5 / 内嵌页 **调用商城** 的会员与积分业务接口 | **§3** |

---

## 1. playX → 积分商城（外部系统调用商城开放接口）

### 1.1 Daily Push API
* 方法：`POST`
* 路径：`/api/v1/mall/dailyPush`

#### Header（多语言，可选）
- `lang`: `zh`/`zh-cn` 返回中文（默认）；`en` 返回英文

#### Header（签名校验：HMAC 必填）
当 `playX.daily_push_secret` 配置非空时，需要携带（HMAC）：
- `X-Request-Id`：请求 ID
- `X-Timestamp`：时间戳
- `X-Signature`：签名（HMAC_SHA256）

服务端签名计算：
- `canonical = X-Timestamp + "\n" + X-Request-Id + "\nPOST\n/api/v1/mall/dailyPush\n" + sha256(json_body)`
- `expected = hash_hmac('sha256', canonical, daily_push_secret)`
- 校验：`hash_equals(expected, X-Signature)`

说明：
- 本项目对接方案为 **仅启用 HMAC**，不使用 `Authorization` 头做校验。

#### Body
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `request_id` | string | 是 | 外部推送请求号（原样返回） |
| `date` | string(YYYY-MM-DD) | 是 | 业务日期（入库到 `mall_daily_push.date`） |
| `user_id` | string | 是 | playX 用户 ID（用于幂等；入库 `mall_daily_push.user_id` 等；服务端会按 `user_id`/`username` **确保存在** `mall_user_asset` 资产行） |
| `username` | string | 否 | 展示冗余（同步到商城用户侧逻辑时使用） |
| `yesterday_win_loss_net` | number | 否 | 昨日净输赢（仅当 `< 0` 时计算新增保障金） |
| `yesterday_total_deposit` | number | 否 | 昨日总充值（用于计算今日可领取上限） |
| `lifetime_total_deposit` | number | 否 | 历史总充值 |
| `lifetime_total_withdraw` | number | 否 | 历史总提现 |

##### 格式 B：新版批量上报（兼容你截图）
新版 body 形如：
```json
{
  "report_date": "1700000000",
  "member": [
    {
      "member_id": "123456",
      "login": "john",
      "lty_deposit": 15230.75,
      "lty_withdrawal": 12400.50,
      "yesterday_total_w": -320.25,
      "yesterday_total_deposit": 500.00
    }
  ]
}
```

字段映射（服务端内部会转换成旧字段再计算）：
- `report_date` -> `date`（若为 Unix 秒则转为 `YYYY-MM-DD`）
- `member[].member_id` -> `user_id`
- `member[].login` -> `username`
- `member[].yesterday_total_w` -> `yesterday_win_loss_net`
- `member[].yesterday_total_deposit` -> `yesterday_total_deposit`
- `member[].lty_deposit` -> `lifetime_total_deposit`
- `member[].lty_withdrawal` -> `lifetime_total_withdraw`

返回补充：
- 批量模式会在 `data` 里增加 `results[]`，每个成员一条结果（是否 `deduped`）。

#### 幂等规则
* 幂等键：`user_id + date`
* 重复推送：不会重复入账，返回 `data.deduped=true`

#### 返回（Response）
外层通用返回结构：`{ code, msg, time, data }`

成功（首次入库）：
| 字段 | 类型 | 说明 |
|------|------|------|
| `data.request_id` | string | 原样返回 |
| `data.accepted` | boolean | `true` |
| `data.deduped` | boolean | `false` |
| `data.message` | string | `ok` |

成功（重复推送）：
| 字段 | 类型 | 说明 |
|------|------|------|
| `data.request_id` | string | 原样返回 |
| `data.accepted` | boolean | `true` |
| `data.deduped` | boolean | `true` |
| `data.message` | string | `duplicate input` |

失败：
* 当缺少必填字段：code=0，msg 为缺少字段错误
* 当签名不正确：HTTP 401，code=0，msg 为 `INVALID_SIGNATURE`

#### 示例（未开启签名校验）
请求：
```bash
curl -X POST 'http://localhost:1818/api/v1/mall/dailyPush' \
  -H 'Content-Type: application/json' \
  -d '{
    "request_id":"req_1001",
    "date":"2026-03-18",
    "user_id":"U123",
    "username":"demo_user",
    "yesterday_win_loss_net":-120.5,
    "yesterday_total_deposit":50,
    "lifetime_total_deposit":5000,
    "lifetime_total_withdraw":2000
  }'
```

响应（首次）：
```json
{
  "code": 1,
  "msg": "",
  "data": {
    "request_id": "req_1001",
    "accepted": true,
    "deduped": false,
    "message": "ok"
  },
  "time": 0
}
```

#### 示例（新版批量上报）
请求：
```bash
curl -X POST 'http://localhost:1818/api/v1/mall/dailyPush' \
  -H 'Content-Type: application/json' \
  -d '{
    "report_date": "1700000000",
    "member": [
      {
        "member_id": "123456",
        "login": "john",
        "lty_deposit": 15230.75,
        "lty_withdrawal": 12400.50,
        "yesterday_total_w": -320.25,
        "yesterday_total_deposit": 500.00
      }
    ]
  }'
```

返回（首次写入至少一个成员时的示例）：
```json
{
  "code": 1,
  "msg": "",
  "time": 0,
  "data": {
    "request_id": "report_2023-11-14",
    "accepted": true,
    "deduped": false,
    "message": "Ok",
    "results": [
      {
        "user_id": "123456",
        "accepted": true,
        "deduped": false,
        "message": "Ok"
      }
    ]
  }
}
```

---

## 2. 积分商城 → playX（贵方需提供的 HTTP 接口）

商城在验 Token、红利发放、交易轮询、Angpow 导入等场景会 **主动请求 playX / Cash Market**。  
**完整 URL、请求/响应字段、成功判定、与 Angpush 双路径关系、联调待办** 已单独整理，便于 **直接转发给 playX 平台**：

- **`docs/playX-接口待完善清单.md`**

本文 **§1** 仅描述「谁调用商城」；**§3** 描述「H5 调用商城」。

---

## 3. 积分商城 → H5（服务端提供给 H5 的接口）

### 3.0 数据模型说明（与代码一致）

* **积分商城用户资产主表**：`mall_user_asset`（账号、积分、`playx_user_id` 等；H5 临时登录 `temLogin` 直接创建/复用该表行，**不依赖**独立会员 `user` 表）。
* **会话缓存**：`mall_session`（字段含 `session_id`、`user_id`（此处存 **playX 侧用户标识字符串**，与 `mall_user_asset.playx_user_id` 一致）、`expire_time` 等）。
* **统一订单**：`mall_order`（红利/实物/提现订单；`user_id` 字段为 **`playx_user_id` 字符串**）。
* **对外业务 ID**：订单与推送中的 `user_id` 多为 **playX 用户 ID**（`playx_user_id`）。临时登录场景下，资产表会生成占位 ID，形如 **`mall_{mall_user_asset.id}`**（见 `MallUserAsset::ensureForUsername`）。

### 3.1 鉴权解析规则（`resolvePlayxAssetIdFromRequest`）

以下接口在服务端最终都会解析出 **`mall_user_asset.id`（整型，资产表主键）**，再按该 ID 加载资产与关联数据。

优先级（由高到低）：

1. **`session_id`**（`post` 优先，`get` 兼容）  
   * 在 `mall_session` 中存在且未过期：用会话里的 `user_id`（`playx_user_id` 字符串）在 `mall_user_asset` 按 `playx_user_id` 查找，得到资产主键。  
   * 若会话无效：兼容把 `session_id` 参数误当作 **商城 token** 再试一次（UUID 形态 token）。
2. **`token`**（`post` / `get` 或请求头 **`ba-token`** / **`token`**）  
   * 校验 `token` 表：类型为会员 `user` 或商城临时 **`muser`**。未过期时，`user_id` 字段为 **`mall_user_asset.id`**（`muser`）或会员体系约定 ID（`user`）。
3. **`user_id`**（`post` / `get` 兼容）  
   * **纯数字**：视为 **`mall_user_asset.id`**。  
   * **非纯数字**：视为 **`playx_user_id`**，在 `mall_user_asset` 按该字段查找主键。

> 注意：请求参数的取值方式是 `post()` 优先，`get()` 兼容（即同字段既可传 post 也可传 get）。

---

### 3.2 临时登录（获取商城 token）

* 方法：`GET`（推荐）或 `POST`
* 路径：`/api/v1/temLogin`
* 开关：`config/buildadmin.php` → `agent_auth.temp_login_enable` 为 `true`；有效期 `agent_auth.temp_login_expire`（秒）。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `username` | string | 是 | 登录名（唯一）；不存在则自动创建 `mall_user_asset` 行 |

#### 行为说明

* 若用户名不存在：`MallUserAsset::ensureForUsername` 创建资产行（随机密码等），并将 `playx_user_id` 更新为 **`mall_{id}`** 形式（与真实 playX ID 区分）。
* 签发 **商城 token**（类型 **`muser`**，`token` 表内 `user_id` = **`mall_user_asset.id`**），并签发 `muser-refresh` 刷新令牌。

#### 返回（成功 data.userInfo）

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | int | **`mall_user_asset.id`** |
| `username` | string | 用户名 |
| `nickname` | string | 同 `username` |
| `playx_user_id` | string | 资产表中的 `playx_user_id`（如 `mall_12`） |
| `token` | string | 访问 H5 接口时携带 |
| `refresh_token` | string | 调用 `/api/common/refreshToken` 时使用（类型 `muser-refresh`） |
| `expires_in` | int | token 有效秒数 |

#### 示例

```bash
curl -G 'http://localhost:1818/api/v1/temLogin' --data-urlencode 'username=demo_h5'
```

用户名含 `+` 等号时需 URL 编码（如 `%2B60123456789`）。

---

### 3.3 Token 验证（换 session）

* 方法：`POST`（推荐 `GET` 传 `token` 亦可）
* 路径：`/api/v1/mall/verifyToken`

#### 配置：本地验证 vs 远程 playX

* 配置项：`config/playx.php` → **`verify_token_local_only`**（环境变量 **`PLAYX_VERIFY_TOKEN_LOCAL_ONLY`**，未设置时默认为 **`1` / 开启本地验证）。
* **`verify_token_local_only = true`（默认）**  
  * **不请求** playX HTTP。  
  * 仅接受商城临时登录 token（类型 **`muser`**），校验 `token` 表后写入 **`mall_session`**。  
  * 返回的 `data.user_id` 为 **`playx_user_id`**（与资产表一致）。
* **`verify_token_local_only = false`**（生产对接 playX）  
  * 需配置 **`PLAYX_TOKEN_VERIFY_URL`**（完整 `https` 回调或相对路径）、**`PLAYX_ANGPOW_MERCHANT_CODE`**、**`PLAYX_ANGPOW_IMPORT_AUTH_KEY`** 等（见 `docs/PlayX-对接文档（积分商城）.md` §5.2）。  
  * 相对路径时尚需 **`PLAYX_ANGPOW_IMPORT_BASE_URL`**；未配置则返回未配置类错误。

#### 请求参数

必填其一：

* `token`（Body 优先；`session` 兼容字段；Query 也可传 `token`）

可选 **`lang`**：`zh` / `ZH` / `zh-cn` 中文 **`msg`**（默认），`en` / `EN` 英文；可通过请求头、Query 或与 Body 同传的字段指定。

#### 返回（成功 data）

| 字段 | 类型 | 说明 |
|------|------|------|
| `session_id` | string | 写入 `mall_session` |
| `user_id` | string | playX 用户 ID（即 `playx_user_id`，会话内与订单/推送一致） |
| `username` | string | 用户名 |
| `token_expire_at` | string | ISO 字符串（服务端 `date('c', expireAt)`） |

失败：

* token 为空：HTTP 401，msg=`INVALID_TOKEN`
* 远程模式且 playX 未配置：`msg=playX API not configured`

#### 示例（本地验证）

```bash
curl -X POST 'http://localhost:1818/api/v1/mall/verifyToken' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'token=上一步TemLogin返回的token'
```

---

### 3.x 收货地址（`mall_address`）

> 下面接口用于 H5 维护收货地址。鉴权同本章其他接口：携带 `session_id` 或 `token` 或 `user_id`。

#### 3.x.1 地址列表
* 方法：`GET`
* 路径：`/api/v1/mall/addressList`

返回：`data.list` 为地址数组。

#### 3.x.2 添加地址
* 方法：`POST`
* 路径：`/api/v1/mall/addressAdd`

Body：
| 字段 | 必填 | 说明 |
|------|------|------|
| `receiver_name` | 是 | 收货人 |
| `phone` | 是 | 电话 |
| `region` | 是 | 地区（数组或逗号分隔字符串） |
| `detail_address` | 是 | 详细地址 |
| `default_setting` | 否 | `1` 设为默认地址 |

#### 3.x.3 修改地址（含设为默认）
* 方法：`POST`
* 路径：`/api/v1/mall/addressEdit`

Body：`id` 必填，其余字段按需传入更新。

#### 3.x.4 删除地址
* 方法：`POST`
* 路径：`/api/v1/mall/addressDelete`

Body：`id` 必填。若删除默认地址，服务端会自动挑选一条剩余地址设为默认（如存在）。

---

### 3.4 用户资产（Assets）
* 方法：`GET`
* 路径：`/api/v1/mall/assets`

#### 请求参数（鉴权）

以下任选其一即可（与 **3.1 鉴权解析规则** 一致）：

* `session_id`
* `token`（或请求头 `ba-token` / `token`）
* `user_id`（纯数字为 **`mall_user_asset.id`**，否则为 **`playx_user_id`**）

#### 返回（成功 data）
若未找到资产：返回 0。
| 字段 | 类型 | 说明 |
|------|------|------|
| `locked_points` | int | 待领取积分 |
| `available_points` | int | 可用积分 |
| `today_limit` | int | 今日可领取上限 |
| `today_claimed` | int | 今日已领取 |
| `withdrawable_cash` | number(2) | `available_points * points_to_cash_ratio`（保留 2 位） |

#### 示例
```bash
curl -G 'http://localhost:1818/api/v1/mall/assets' --data-urlencode 'token=上一步temLogin返回的token'
```

响应（示例）：
```json
{
  "code": 1,
  "msg": "",
  "time": 1712345678,
  "data": {
    "locked_points": 100,
    "available_points": 50,
    "today_limit": 200,
    "today_claimed": 80,
    "withdrawable_cash": 5.2
  }
}
```

---

### 3.5 领取（Claim）
* 方法：`POST`
* 路径：`/api/v1/mall/claim`

#### 请求 Body
必填：

* `claim_request_id`：幂等键（string，唯一）

鉴权：同 **3.1**（`session_id` / `token` / `user_id`）

#### 返回（成功 data）
与 `用户资产` 返回字段一致（资产快照）。

幂等：
* `claim_request_id` 已存在：不会重复入账，直接返回当前资产快照

#### 示例
```bash
curl -X POST 'http://localhost:1818/api/v1/mall/claim' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'claim_request_id=claim_001' \
  --data-urlencode 'token=上一步temLogin返回的token'
```

响应（首次领取，示例）：
```json
{
  "code": 1,
  "msg": "Claim success",
  "time": 1712345679,
  "data": {
    "locked_points": 60,
    "available_points": 90,
    "today_limit": 200,
    "today_claimed": 120,
    "withdrawable_cash": 9.0
  }
}
```

响应（幂等重复，示例：可能 msg 为空）：
```json
{
  "code": 1,
  "msg": "",
  "time": 1712345680,
  "data": {
    "locked_points": 60,
    "available_points": 90,
    "today_limit": 200,
    "today_claimed": 120,
    "withdrawable_cash": 9.0
  }
}
```

---

### 3.6 商品列表
* 方法：`GET`
* 路径：`/api/v1/mall/items`

#### 请求参数
* `type`（可选）：`BONUS` | `PHYSICAL` | `WITHDRAW`

不传或空：返回 `mall_item.status=1` 且不过滤 type 的商品列表。

#### 返回（成功 data）
* `list`：商品列表（直接返回 `MallItem` 的字段数组；包含扩展字段：`amount/multiplier/category/category_title` 等）

#### 示例
请求：
```bash
curl -G 'http://localhost:1818/api/v1/mall/items' --data-urlencode 'type=WITHDRAW'
```

响应（示例）：
```json
{
  "code": 1,
  "msg": "",
  "time": 1712345685,
  "data": {
    "list": [
      {
        "id": 321,
        "title": "提现档位A",
        "type": 3,
        "score": 1000,
        "amount": 100.0,
        "multiplier": 1,
        "category": "withdraw",
        "category_title": "提现"
      }
    ]
  }
}
```

---

### 3.7 红利兑换（Bonus Redeem）
* 方法：`POST`
* 路径：`/api/v1/mall/bonusRedeem`

#### 请求 Body
必填：
* `item_id`：商品 ID（要求 `mall_item.type=BONUS` 且 `status=1`）
鉴权：同 **3.1**（`session_id` / `token` / `user_id`）

#### 返回（成功）
* `msg`：`Redeem submitted, please wait about 10 minutes`
* `data.order_id`：订单 ID
* `data.status`：`PENDING`

#### 示例
```bash
curl -X POST 'http://localhost:1818/api/v1/mall/bonusRedeem' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'item_id=123' \
  --data-urlencode 'session_id=7b1c....'
```

响应（示例）：
```json
{
  "code": 1,
  "msg": "Redeem submitted, please wait about 10 minutes",
  "time": 1712345686,
  "data": {
    "order_id": 456,
    "status": "PENDING"
  }
}
```

---

### 3.8 实物兑换（Physical Redeem）
* 方法：`POST`
* 路径：`/api/v1/mall/physicalRedeem`

#### 请求 Body
必填：
* `item_id`：商品 ID（要求 `mall_item.type=PHYSICAL` 且 `status=1`）
* `address_id`：收货地址 ID（`mall_address.id`，须属于当前用户资产；下单时写入 `mall_order.mall_address_id`，并将该地址快照写入 `receiver_name` / `receiver_phone` / `receiver_address`）
鉴权：同 **3.1**（`session_id` / `token` / `user_id`）

#### 返回（成功）
* `msg`：`Redeem success`
* `data`：`null`

#### 示例
```bash
curl -X POST 'http://localhost:1818/api/v1/mall/physicalRedeem' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'item_id=200' \
  --data-urlencode 'address_id=10' \
  --data-urlencode 'session_id=7b1c....'
```

响应（示例）：
```json
{
  "code": 1,
  "msg": "Redeem success",
  "time": 1712345687,
  "data": null
}
```

---

### 3.9 提现申请（Withdraw Apply）
* 方法：`POST`
* 路径：`/api/v1/mall/withdrawApply`

#### 请求 Body
必填：
* `item_id`：商品 ID（要求 `mall_item.type=WITHDRAW` 且 `status=1`）
鉴权：同 **3.1**（`session_id` / `token` / `user_id`）

#### 返回（成功）
* `msg`：`Withdraw submitted, please wait about 10 minutes`
* `data.order_id`：订单 ID
* `data.status`：`PENDING`

#### 示例
```bash
curl -X POST 'http://localhost:1818/api/v1/mall/withdrawApply' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'item_id=321' \
  --data-urlencode 'session_id=7b1c....'
```

响应（示例）：
```json
{
  "code": 1,
  "msg": "Withdraw submitted, please wait about 10 minutes",
  "time": 1712345688,
  "data": {
    "order_id": 789,
    "status": "PENDING"
  }
}
```

---

### 3.10 订单列表
* 方法：`GET`
* 路径：`/api/v1/mall/orders`

#### 请求参数（鉴权）

同 **3.1**（`session_id` / `token` / `user_id`）。

#### 返回（成功 data）

* `list`：订单列表（最多 100 条），并包含关联的 `mallItem`（关系对象）
* 列表项中的 `user_id` 为 **playX 侧 `playx_user_id`**（字符串），与 `mall_order.user_id` 一致

#### 示例
请求：
```bash
curl -G 'http://localhost:1818/api/v1/mall/orders' --data-urlencode 'token=上一步temLogin返回的token'
```

响应（示例，简化）：
```json
{
  "code": 1,
  "msg": "",
  "time": 1712345689,
  "data": {
    "list": [
      {
        "id": 456,
        "user_id": "U123",
        "type": "BONUS",
        "status": "PENDING",
        "mall_item_id": 123,
        "points_cost": 100,
        "amount": 10.0,
        "external_transaction_id": "BONUS_ORD2026....",
        "grant_status": "NOT_SENT",
        "mallItem": {
          "id": 123,
          "title": "每日红利",
          "type": 1
        }
      }
    ]
  }
}
```

---

### 3.11 积分流水（Points Logs）
* 方法：`GET`
* 路径：`/api/v1/mall/pointsLogs`

#### 请求参数（鉴权）
同 **3.1**（`session_id` / `token` / `user_id`）。

#### Query 参数
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `limit` | int | 否 | 每页条数，默认 20，最大 100 |
| `cursor` | string | 否 | 游标（上一页返回 `next_cursor`） |
| `direction` | string | 否 | `IN` 仅入账 / `OUT` 仅扣减；不传返回全部 |

#### 返回（成功 data）
| 字段 | 类型 | 说明 |
|------|------|------|
| `data.list` | array | 流水数组（时间倒序） |
| `data.next_cursor` | string\|null | 下一页游标（本页最后一条记录的游标） |

`list` 每一项字段：
| 字段 | 类型 | 说明 |
|------|------|------|
| `biz_type` | string | `CLAIM`/`REDEEM_BONUS`/`REDEEM_PHYSICAL`/`REDEEM_WITHDRAW`/`REFUND` |
| `direction` | string | `IN`/`OUT` |
| `points` | int | 积分变动值（正数，方向由 `direction` 表示） |
| `ts` | int | Unix 秒 |
| `ref_id` | string | 领取为 `claim_request_id`；订单为 `external_transaction_id` |
| `order_no` | string | 订单号（非订单类为空） |
| `order_status` | string | 订单状态（非订单类为空） |
| `mallItem` | object\|null | 商品信息（非订单类为 null） |
| `item_id` | int | 兼容字段：商品ID（非订单类为 0） |
| `item_title` | string | 兼容字段：商品标题（非订单类为空） |
| `item_type` | int | 兼容字段：商品类型（非订单类为 0；`1=BONUS 2=PHYSICAL 3=WITHDRAW`） |
| `item_score` | int | 兼容字段：商品所需积分（非订单类为 0） |
| `cursor` | string | 当前记录游标 |

`mallItem` 字段（非隐私）：
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | int | `mall_item.id` |
| `title` | string | 商品名 |
| `type` | int | 商品类型 |
| `score` | int | 所需积分 |
| `amount` | number | 现金面值 |
| `multiplier` | int | 流水倍数 |
| `category` | string | 红利业务类别 |
| `category_title` | string | 类别展示名 |

#### 示例
```bash
curl -G 'http://localhost:1818/api/v1/mall/pointsLogs' \
  --data-urlencode 'token=上一步temLogin返回的token' \
  --data-urlencode 'limit=20'
```

---

### 3.12 同步额度（可选）
当前代码未实现并未注册路由：`/api/v1/mall/syncLimit`。

