Files
webman-buildadmin-mall/docs/PlayX-接口文档.md
2026-04-21 16:00:02 +08:00

712 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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=0msg 为缺少字段错误
* 当签名不正确HTTP 401code=0msg 为 `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.api.base_url`**,由商城向 playX 发起 `POST` 校验(请求/响应约定见 **`docs/playX-接口待完善清单.md`** 第一部分 §1
* 若未配置 `base_url`,返回 `playX API not configured`
#### 请求参数
必填其一:
* `token`Body 优先;`session` 兼容字段Query 也可传 `token`
#### 返回(成功 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 401msg=`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`