# 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.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 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`。