# playX 调用积分商城接口说明

本文档描述 **playX 平台（或 playX 侧脚本/服务）如何调用积分商城已开放的 HTTP 接口**：基础约定、推荐流程、鉴权方式、请求参数与返回结构。

实现依据：`config/route.php`、`app/api/controller/v1/Playx.php`、`config/playx.php`。

---

## 1. 基础约定

### 1.1 Base URL

将下列路径拼在积分商城对外域名之后，例如：

`https://{商城域名}/api/v1/mall/dailyPush`

（联调时请向商城方索取正式环境与测试环境地址。）

### 1.2 通用响应结构（JSON）

所有接口成功或失败，响应体均为：

| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | int | `1` 表示业务成功；`0` 表示业务失败 |
| `msg` | string | 提示信息（失败时为错误原因） |
| `time` | int | Unix 时间戳（秒） |
| `data` | object/array/null | 业务数据；失败时可能为 `null` |

部分错误场景会通过 HTTP 状态码区分（如 **401**），此时 `code` 仍为 `0`，请同时判断 HTTP 状态与 `code`。

**成功示例：**

```json
{
  "code": 1,
  "msg": "",
  "time": 1730000000,
  "data": { }
}
```

**失败示例：**

```json
{
  "code": 0,
  "msg": "错误原因",
  "time": 1730000000,
  "data": null
}
```

### 1.3 Content-Type

- 本文档中 **POST** 且带 JSON Body 的接口，请使用：`Content-Type: application/json`。

### 1.4 多语言（响应文案）

可通过请求头 `lang` 控制返回文案语言：

| Header | 值 | 说明 |
|--------|----|------|
| `lang` | `zh` / `zh-cn` | 返回中文（默认） |
| `lang` | `en` | 返回英文 |

---

## 2. 使用流程（推荐）

### 2.1 playX 服务端 → 商城：每日数据推送（主流程）

适用于 T+1 等业务数据由 **playX 服务端**主动推送到积分商城。

```mermaid
sequenceDiagram
    participant PX as playX 服务端
    participant M as 积分商城

    Note over PX,M: 按约定配置 HMAC 密钥
    PX->>M: POST /api/v1/mall/dailyPush（JSON Body）
    M-->>PX: code=1, data.accepted / deduped
```

1. 与商城方约定 **商城 Base URL** 与 **HMAC**（`X-Signature` 等）密钥。
2. 按 **§3** 构造请求并推送。
3. 根据返回 `data.deduped` 判断是否为幂等重复推送。

### 2.2 用户侧（H5 / 内嵌页）→ 商城：会话与业务接口

以下接口多由 **用户在浏览器内**打开积分商城 H5 后调用，通过 **`session_id`**（先调 `verifyToken` 获取）或 **`token`**（商城 `muser` 类 token）标识用户，**不一定由 playX 后端直接调用**：

- `POST /api/v1/mall/verifyToken`：用 playX token 换商城 `session_id`
- `GET /api/v1/mall/assets`：查询资产
- `POST /api/v1/mall/claim`：领取积分
- `GET /api/v1/mall/items`：商品列表
- `POST /api/v1/mall/bonusRedeem` / `physicalRedeem` / `withdrawApply`：兑换与提现申请
- `GET /api/v1/mall/orders`：订单列表

若 playX 后端需要代替用户调用上述接口，须同样携带有效的 `session_id` 或 `token`，并遵守同一用户身份规则（见 **§4 身份说明**）。

---

## 3. playX 服务端推送：Daily Push

### 3.1 概要

| 项目 | 值 |
|------|-----|
| 方法 | `POST` |
| 路径 | `/api/v1/mall/dailyPush` |

### 3.2 鉴权（按商城部署配置，可组合）

#### 推荐方案：仅启用 HMAC（当前对接采用）

商城侧配置：设置环境变量 **`PLAYX_DAILY_PUSH_SECRET`** 为非空（启用 HMAC 校验）。

#### HMAC 签名（必填）

当商城配置 **`PLAYX_DAILY_PUSH_SECRET`** 非空时，需同时携带：

| Header | 说明 |
|--------|------|
| `X-Request-Id` | 请求 ID（建议与 Body 内可追溯字段一致） |
| `X-Timestamp` | Unix 时间戳（秒，字符串） |
| `X-Signature` | 签名（十六进制小写或大写需与实现一致，以下为十六进制字符串） |

签名原文与计算：

```
canonical = X-Timestamp + "\n" + X-Request-Id + "\nPOST\n/api/v1/mall/dailyPush\n" + sha256(json_body)
expected  = HMAC_SHA256( canonical , PLAYX_DAILY_PUSH_SECRET )
```

其中 `json_body` 为 **实际发送的 JSON 原始字符串** 计算出的 SHA256（十六进制）；与 PHP `hash('sha256', $rawBody)` 一致。

校验：`hash_equals(expected, X-Signature)`。

#### Header 填写清单（HMAC 模式）
- 必填：`X-Request-Id`、`X-Timestamp`、`X-Signature`

#### 重要注意：`json_body` 必须与实际发送一致
为了保证签名可验通过：用于计算 sha256 的 `json_body` 必须是**实际发送到 HTTP body 的原始 JSON 字符串**（字节级一致）。\
建议：在发送端先序列化 JSON 得到字符串 `rawBody`，用该 `rawBody` 做 sha256 与 HMAC，再把同一个 `rawBody` 作为请求 body 发送。

### 3.3 Body 参数（JSON）

`/api/v1/mall/dailyPush` 支持 **两种入参格式**（按字段自动识别）：

#### 格式 A：旧版单条上报（兼容）
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `request_id` | string | 是 | 本次推送请求号；响应中原样返回 |
| `date` | string | 是 | 业务日期，格式 `YYYY-MM-DD` |
| `user_id` | string | 是 | playX 用户 ID（幂等键之一） |
| `username` | string | 否 | 展示名；用于同步/创建商城侧用户资产展示信息 |
| `yesterday_win_loss_net` | number | 否 | 昨日净输赢；**小于 0** 时按配置比例计入待领取保障金（`locked_points`） |
| `yesterday_total_deposit` | number | 否 | 昨日总充值；用于计算当日可领取上限等 |
| `lifetime_total_deposit` | number | 否 | 历史总充值（冗余入库） |
| `lifetime_total_withdraw` | number | 否 | 历史总提现（冗余入库） |

#### 格式 B：新版批量上报（你图中格式）
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `report_date` | string/number | 是 | 报表日期；可以为 Unix 秒时间戳（如 `1700000000`）或 `YYYY-MM-DD` |
| `member` | array | 是 | 成员列表，每个成员包含一名 playX 用户数据 |

成员元素字段：
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `member_id` | string | 是 | playX 用户 ID（幂等键之一） |
| `login` | string | 否 | 用户展示名 |
| `yesterday_total_w` | number | 否 | 昨日净输赢；小于 0 才会累加到 `locked_points` |
| `yesterday_total_deposit` | number | 否 | 昨日总充值；用于计算 `today_limit` |
| `lty_deposit` | number | 否 | 历史总充值（冗余入库） |
| `lty_withdrawal` | number | 否 | 历史总提现（冗余入库） |

#### Body 填写要求（批量模式）
- **必须有**：`report_date`、`member`（数组且至少 1 个元素）、`member[].member_id`。
- **允许缺省**：成员的 `login/yesterday_total_w/yesterday_total_deposit/lty_deposit/lty_withdrawal`；缺省时按 `0` 或空字符串处理。
- **日期**：`report_date` 传 Unix 秒会自动转换成 `YYYY-MM-DD`；如果直接传 `YYYY-MM-DD` 也支持。

### 3.4 幂等

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

### 3.5 返回 `data` 字段

| 字段 | 类型 | 说明 |
|------|------|------|
| `request_id` | string | 与请求一致 |
| `accepted` | bool | 是否受理成功 |
| `deduped` | bool | 是否为重复推送（幂等命中） |
| `message` | string | 说明文案 |

#### 格式 B：批量上报的返回补充
批量模式会在 `data` 中增加：`results`。

`data.results` 为数组，元素字段如下：
| 字段 | 类型 | 说明 |
|------|------|------|
| `user_id` | string | 对应成员的 `member_id` |
| `accepted` | bool | 是否受理成功 |
| `deduped` | bool | 该成员是否为重复推送 |
| `message` | string | `ok` 或 `duplicate input` |

**HTTP 401**：HMAC 不通过（签名缺失/不完整/校验失败）。

### 3.6 请求示例

```bash
curl -X POST 'https://{商城域名}/api/v1/mall/dailyPush' \
  -H 'Content-Type: application/json' \
  -H 'X-Request-Id: req_1700000000_123456' \
  -H 'X-Timestamp: 1700000000' \
  -H 'X-Signature: <按本文档 canonical 计算出的 HMAC_SHA256>' \
  -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"
      }
    ]
  }
}
```

---

## 4. 身份说明（`session_id` / `token` / `user_id`）

以下接口通过 **`resolvePlayxAssetIdFromRequest`** 解析当前用户，优先级如下：

1. **`session_id`**（POST/GET）：对应商城表 `mall_playx_session`，未过期则映射到 `mall_playx_user_asset`。
2. 若 `session_id` 实际是 **`muser` 类型 token**（历史兼容），也会按 token 解析。
3. **`token`**（POST/GET 或标准鉴权头）：商城 token 表内类型为会员或 **`muser`** 且未过期时，`user_id` 为 **`mall_playx_user_asset.id`**（资产表主键）。
4. **`user_id`**（POST/GET）：  
   - 若**纯数字**：视为 **`mall_playx_user_asset.id`**；  
   - 否则：按 **`playx_user_id`** 查找资产行。

无法解析身份时，通常返回 **401** 或参数错误提示。

---

## 5. 其他接口一览（前端联调版）

> 下列接口统一返回 `code/msg/time/data`；成功通常为 `code=1`。  
> 除 `verifyToken` 外，其余用户接口均需携带 `session_id` 或 `token`（见 §4）。

### 5.0 `POST /api/v1/mall/dailyPush`（后端对后端）
**用途**：playX 按日推送用户资产基础数据到商城（前端一般不直接调用）。

**请求示例：**
```json
{
  "request_id": "report_20260430",
  "rows": [
    {
      "user_id": "U10001",
      "username": "demo_user",
      "yesterday_total_deposit": 1000,
      "yesterday_win_loss_net": -500
    }
  ]
}
```

### 5.1 `POST /api/v1/mall/verifyToken`
**用途**：把 playX token 换成商城 `session_id`。

**语言 `lang`（响应 `msg` 等多语言）**：
- **默认中文**（`zh-cn`）。
- 可通过 **请求头 `lang`**、**Query `lang`** 或 **表单/JSON 字段 `lang`** 指定：`zh` / `ZH` / `zh-cn` → 中文，`en` / `EN` → 英文（与 `LoadLangPack` 规则一致）。

**前端入参约定（重要）**：
- 调用本接口时 **仅需 `token`**（或兼容 `session`）；**不要**传 `request_id` / `request_date` / 商户字段（均由商城后端生成后请求上游）。
- 建议 **`Content-Type: application/json`**，Body：`{"token":"..."}`。若使用 **form-data**（如图二），请同样只传 `token`（及可选 `lang`）；避免缺少 `token` 键名。
- 商城请求上游回调地址时：Body 含 **`merchant_code`、`request_date`（秒）、`request_id`、`token`**；`X-Request-Signature` 与 angpow-imports **同源算法**，canonical 为 **`merchant_code=...&request_date=...&request_id=...&token=...`**。

**请求示例：**
```json
{
  "token": "eyJhbGciOi..."
}
```

带英文响应示例（Query）：

```http
POST /api/v1/mall/verifyToken?lang=en
Content-Type: application/json

{"token":"eyJhbGciOi..."}
```

**成功响应示例：**
```json
{
  "code": 1,
  "msg": "Success",
  "time": 1777533000,
  "data": {
    "session_id": "fc7f3e3f0d0f4cb29f66e4c8fbab4f66",
    "user_id": "U10001",
    "username": "demo_user",
    "token_expire_at": "2026-04-30T16:20:00+08:00"
  }
}
```

### 5.2 `GET /api/v1/mall/assets`
**用途**：查询当前用户积分资产。

**请求示例：**
```http
GET /api/v1/mall/assets?session_id=fc7f3e3f0d0f4cb29f66e4c8fbab4f66
```

**成功响应示例：**
```json
{
  "code": 1,
  "msg": "Success",
  "time": 1777533000,
  "data": {
    "locked_points": 120,
    "available_points": 350,
    "today_limit": 200,
    "today_claimed": 80,
    "withdrawable_cash": 35
  }
}
```

### 5.3 `POST /api/v1/mall/claim`
**用途**：领取积分（幂等）。

**请求示例：**
```json
{
  "session_id": "fc7f3e3f0d0f4cb29f66e4c8fbab4f66",
  "claim_request_id": "claim_20260430_0001"
}
```

**成功响应示例：**
```json
{
  "code": 1,
  "msg": "Success",
  "time": 1777533000,
  "data": {
    "locked_points": 60,
    "available_points": 410,
    "today_limit": 200,
    "today_claimed": 140,
    "withdrawable_cash": 41
  }
}
```

### 5.4 `GET /api/v1/mall/items`
**用途**：获取商城商品列表（可按类型筛选）。

**请求示例：**
```http
GET /api/v1/mall/items?type=BONUS
```

**成功响应示例：**
```json
{
  "code": 1,
  "msg": "Success",
  "time": 1777533000,
  "data": {
    "list": [
      {
        "id": 101,
        "title": "10 MYR Bonus",
        "type": "BONUS",
        "points_cost": 1000
      }
    ]
  }
}
```

### 5.5 `POST /api/v1/mall/bonusRedeem`
**用途**：兑换红利商品。

**请求示例：**
```json
{
  "session_id": "fc7f3e3f0d0f4cb29f66e4c8fbab4f66",
  "item_id": 101
}
```

**成功响应示例：**
```json
{
  "code": 1,
  "msg": "Success",
  "time": 1777533000,
  "data": {
    "order_id": "ORD202604300001",
    "status": "PENDING"
  }
}
```

### 5.6 `POST /api/v1/mall/physicalRedeem`
**用途**：兑换实物商品（需要地址）。

**请求示例：**
```json
{
  "session_id": "fc7f3e3f0d0f4cb29f66e4c8fbab4f66",
  "item_id": 202,
  "address_id": 12
}
```

**成功响应示例：**
```json
{
  "code": 1,
  "msg": "Success",
  "time": 1777533000,
  "data": {
    "order_id": "ORD202604300002",
    "status": "PENDING"
  }
}
```

### 5.7 `POST /api/v1/mall/withdrawApply`
**用途**：发起提现档位兑换申请。

**请求示例：**
```json
{
  "session_id": "fc7f3e3f0d0f4cb29f66e4c8fbab4f66",
  "item_id": 303
}
```

**成功响应示例：**
```json
{
  "code": 1,
  "msg": "Success",
  "time": 1777533000,
  "data": {
    "order_id": "ORD202604300003",
    "status": "PENDING"
  }
}
```

### 5.8 `GET /api/v1/mall/orders`
**用途**：查询当前用户订单列表。

**请求示例：**
```http
GET /api/v1/mall/orders?session_id=fc7f3e3f0d0f4cb29f66e4c8fbab4f66
```

**成功响应示例：**
```json
{
  "code": 1,
  "msg": "Success",
  "time": 1777533000,
  "data": {
    "list": [
      {
        "order_id": "ORD202604300001",
        "status": "PENDING",
        "item_title": "10 MYR Bonus"
      }
    ]
  }
}
```

### 5.9 `GET /api/v1/mall/pointsLogs`
**用途**：查询积分变动日志。

**请求示例：**
```http
GET /api/v1/mall/pointsLogs?session_id=fc7f3e3f0d0f4cb29f66e4c8fbab4f66
```

**成功响应示例：**
```json
{
  "code": 1,
  "msg": "Success",
  "time": 1777533000,
  "data": {
    "list": [
      {
        "id": 9001,
        "change_points": 50,
        "type": "CLAIM",
        "remark": "daily claim"
      }
    ]
  }
}
```

### 5.10 收货地址（`mall_address`）
**用途**：用户收货地址 CRUD。

#### 5.10.1 `GET /api/v1/mall/addressList`
**请求示例：**
```http
GET /api/v1/mall/addressList?session_id=fc7f3e3f0d0f4cb29f66e4c8fbab4f66
```

**成功响应示例：**
```json
{
  "code": 1,
  "msg": "Success",
  "time": 1777533000,
  "data": {
    "list": [
      {
        "id": 12,
        "receiver_name": "Tom",
        "phone": "0123456789",
        "detail_address": "KLCC",
        "default_setting": 1
      }
    ]
  }
}
```

#### 5.10.2 `POST /api/v1/mall/addressAdd`
**请求示例：**
```json
{
  "session_id": "fc7f3e3f0d0f4cb29f66e4c8fbab4f66",
  "receiver_name": "Tom",
  "phone": "0123456789",
  "region": "Kuala Lumpur,KLCC",
  "detail_address": "Tower A, 8F",
  "default_setting": 1
}
```

**成功响应示例：**
```json
{
  "code": 1,
  "msg": "Success",
  "time": 1777533000,
  "data": {
    "id": 13
  }
}
```

#### 5.10.3 `POST /api/v1/mall/addressEdit`
**请求示例：**
```json
{
  "session_id": "fc7f3e3f0d0f4cb29f66e4c8fbab4f66",
  "id": 13,
  "detail_address": "Tower B, 10F",
  "default_setting": 1
}
```

**成功响应示例：**
```json
{
  "code": 1,
  "msg": "Success",
  "time": 1777533000,
  "data": null
}
```

#### 5.10.4 `POST /api/v1/mall/addressDelete`
**请求示例：**
```json
{
  "session_id": "fc7f3e3f0d0f4cb29f66e4c8fbab4f66",
  "id": 13
}
```

**成功响应示例：**
```json
{
  "code": 1,
  "msg": "Success",
  "time": 1777533000,
  "data": null
}
```

---

## 6. 配置项（供运维/对方技术对照）

| 环境变量 / 配置 | 作用 |
|-----------------|------|
| `PLAYX_DAILY_PUSH_SECRET` | 非空则 Daily Push 必须带合法 HMAC 头 |
| `PLAYX_VERIFY_TOKEN_LOCAL_ONLY` | 为 true 时 verifyToken 不请求 playX 远程 |
| `PLAYX_ANGPOW_IMPORT_BASE_URL` | Angpow 推送等接口基地址；**相对路径** `verify-token` 时亦作其基地址 |
| `PLAYX_TOKEN_VERIFY_URL` | **完整 `https://` URL**：回调 Body `merchant_code`+`request_date`+`request_id`+`token`，签名明文同序；**相对路径**：拼基地址 + 商户四字段 |
| `PLAYX_ANGPOW_MERCHANT_CODE` | **回调与相对路径** `verify-token` 均需 |
| `PLAYX_ANGPOW_IMPORT_AUTH_KEY` | 回调与相对路径的 HMAC 密钥（与 angpow-imports 相同） |

---

## 7. 版本与变更

- 文档与仓库代码同步维护；接口路径以 `config/route.php` 为准。
- 若后续升级鉴权策略（例如叠加 JWT），以部署环境变量与最新文档为准。
