# PlayX 接口文档(按调用方向拆分) 说明:本文档严格依据当前代码 `app/api/controller/v1/Playx.php` 与定时任务 `app/process/PlayxJobs.php` 整理。 三类接口分别为: - `积分商城 -> PlayX`(PlayX 调用商城) - `PlayX -> 积分商城`(商城调用 PlayX) - `积分商城 -> H5`(H5 调用商城) --- ## 1. 积分商城 -> PlayX(PlayX 调用商城) ### 1.1 Daily Push API * 方法:`POST` * 路径:`/api/v1/playx/daily-push` #### Header(签名校验:可选) 当 `playx.daily_push_secret` 配置非空时,需要携带: - `X-Request-Id`:请求 ID - `X-Timestamp`:时间戳 - `X-Signature`:签名(HMAC_SHA256) 服务端签名计算: - `canonical = X-Timestamp + "\n" + X-Request-Id + "\nPOST\n/api/v1/playx/daily-push\n" + sha256(json_body)` - `expected = hash_hmac('sha256', canonical, daily_push_secret)` - 校验:`hash_equals(expected, X-Signature)` #### Body | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `request_id` | string | 是 | 外部推送请求号(原样返回) | | `date` | string(YYYY-MM-DD) | 是 | 业务日期(入库到 `mall_playx_daily_push.date`) | | `user_id` | string | 是 | PlayX 用户 ID(用于幂等) | | `username` | string | 否 | 展示冗余 | | `yesterday_win_loss_net` | number | 否 | 昨日净输赢(仅当 `< 0` 时计算新增保障金) | | `yesterday_total_deposit` | number | 否 | 昨日总充值(用于计算今日可领取上限) | | `lifetime_total_deposit` | number | 否 | 历史总充值 | | `lifetime_total_withdraw` | number | 否 | 历史总提现 | #### 幂等规则 * 幂等键:`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/playx/daily-push' \ -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 } ``` --- ## 2. PlayX -> 积分商城(商城调用 PlayX) > 下面这些接口由 PlayX 提供。商城侧仅按“请求参数 + 期望返回判定条件”发起调用与处理结果。 ### 2.1 Token Verification API * 方法:`POST` * URL:`${playx.api.base_url}${playx.api.token_verify_url}` * 默认:`/api/v1/auth/verify-token` #### 请求 Body(商城侧发送) | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `request_id` | string | 是 | 形如 `mall_{uniqid}` | | `token` | string | 是 | 前端传入的 PlayX token | #### 返回(期望) 商城侧校验: * HTTP 状态码必须为 `200` * 且响应体中必须包含 `user_id` 期望字段(示例): | 字段 | 类型 | 说明 | |------|------|------| | `user_id` | string | 必选 | | `username` | string | 可选 | | `token_expire_at` | string | 可选(能被 `strtotime` 解析) | 示例(成功): ```json { "user_id": "U123", "username": "demo_user", "token_expire_at": "2026-04-01T12:00:00Z" } ``` 示例(失败): ```json { "message": "invalid token" } ``` --- ### 2.2 Bonus Grant API * 方法:`POST` * URL:`${playx.api.base_url}${playx.api.bonus_grant_url}` * 默认:`/api/v1/bonus/grant` #### 请求 Body(商城侧发送) | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `request_id` | string | 是 | 形如 `mall_bonus_{uniqid}` | | `externalTransactionId` | string | 是 | `MallPlayxOrder.external_transaction_id` | | `user_id` | string | 是 | PlayX 用户 ID | | `amount` | number | 是 | `MallPlayxOrder.amount` | | `rewardName` | string | 是 | `mall_item.title` | | `category` | string | 是 | `mall_item.category`(默认 `daily`) | | `categoryTitle` | string | 是 | `mall_item.category_title` | | `multiplier` | int | 是 | `MallPlayxOrder.multiplier` | #### 返回(期望) 商城侧判定: * HTTP 状态码 `200` * 且 `data.status === "accepted"` 成功时读取: * `data.playx_transaction_id` 失败时读取: * `data.message` 写入订单 `fail_reason` 示例(accepted): ```json { "status": "accepted", "playx_transaction_id": "PX_TX_001" } ``` 示例(rejected): ```json { "status": "rejected", "message": "insufficient balance" } ``` --- ### 2.3 Balance Credit API * 方法:`POST` * URL:`${playx.api.base_url}${playx.api.balance_credit_url}` * 默认:`/api/v1/balance/credit` #### 请求 Body(商城侧发送) | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `request_id` | string | 是 | 形如 `mall_withdraw_{uniqid}` | | `externalTransactionId` | string | 是 | `MallPlayxOrder.external_transaction_id` | | `user_id` | string | 是 | PlayX 用户 ID | | `amount` | number | 是 | `MallPlayxOrder.amount` | | `multiplier` | int | 是 | `MallPlayxOrder.multiplier` | #### 返回(期望) 与 Bonus Grant 一致: * `data.status === "accepted"` -> 读取 `playx_transaction_id` * 否则 -> 读取 `message` 写入 `fail_reason` 示例(accepted): ```json { "status": "accepted", "playx_transaction_id": "PX_TX_002" } ``` 示例(rejected): ```json { "status": "rejected", "message": "insufficient balance" } ``` --- ### 2.4 Transaction Status Query API(交易终态查询) * 方法:`GET` * URL:`${playx.api.base_url}${playx.api.transaction_status_url}` * 默认:`/api/v1/transaction/status` #### Query 参数 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `externalTransactionId` | string | 是 | 订单幂等键 `external_transaction_id` | #### 返回(期望) 定时任务读取 `data.status`: * `COMPLETED`:商城将订单 `status` 更新为 `COMPLETED` * `FAILED` 或 `REJECTED`:商城将订单 `status=REJECTED`、`grant_status=FAILED_FINAL`,并退回积分 * 失败信息取 `data.message` 写入订单 `fail_reason` 示例(completed): ```json { "status": "COMPLETED" } ``` 示例(failed): ```json { "status": "FAILED", "message": "grant rejected by PlayX" } ``` --- ## 3. 积分商城 -> H5(服务端提供给 H5 的接口) 说明:鉴权与用户解析规则由 `resolveUserIdFromRequest()` 决定。 * 优先使用 `session_id`(在 `mall_playx_session` 查到且未过期) * 其次使用 `user_id` 公共鉴权字段: * `session_id`:字符串 * `user_id`:字符串 > 注意:请求参数的取值方式是 `post()` 优先,`get()` 兼容(即同字段既可传 post 也可传 get)。 --- ### 3.1 Token 验证 * 方法:`POST` * 路径:`/api/v1/playx/verify-token` #### 请求 Body 必填其一: * `token`(优先读取) * `session`(兼容字段,当 `token` 为空时会被当作 token) #### 返回(成功 data) | 字段 | 类型 | 说明 | |------|------|------| | `session_id` | string | 写入 `mall_playx_session` | | `user_id` | string | PlayX 用户 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/playx/verify-token' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'token=PLAYX_TOKEN_XXX' ``` 响应(成功示例): ```json { "code": 1, "msg": "", "data": { "session_id": "7b1c....", "user_id": "U123", "username": "demo_user", "token_expire_at": "2026-04-01T12:00:00+00:00" } } ``` --- ### 3.2 用户资产 * 方法:`GET` * 路径:`/api/v1/playx/assets` #### 请求参数(鉴权) * `session_id`(优先) * `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/playx/assets' --data-urlencode 'session_id=7b1c....' ``` 响应(示例): ```json { "code": 1, "msg": "", "time": 1712345678, "data": { "locked_points": 100, "available_points": 50, "today_limit": 200, "today_claimed": 80, "withdrawable_cash": 5.2 } } ``` --- ### 3.3 领取(Claim) * 方法:`POST` * 路径:`/api/v1/playx/claim` #### 请求 Body 必填: * `claim_request_id`:幂等键(string,唯一) 鉴权: * `session_id` 或 `user_id` #### 返回(成功 data) 与 `用户资产` 返回字段一致(资产快照)。 幂等: * `claim_request_id` 已存在:不会重复入账,直接返回当前资产快照 #### 示例 ```bash curl -X POST 'http://localhost:1818/api/v1/playx/claim' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'claim_request_id=claim_001' \ --data-urlencode 'session_id=7b1c....' ``` 响应(首次领取,示例): ```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.4 商品列表 * 方法:`GET` * 路径:`/api/v1/playx/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/playx/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.5 红利兑换(Bonus Redeem) * 方法:`POST` * 路径:`/api/v1/playx/bonus/redeem` #### 请求 Body 必填: * `item_id`:商品 ID(要求 `mall_item.type=BONUS` 且 `status=1`) 鉴权: * `session_id` 或 `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/playx/bonus/redeem' \ -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.6 实物兑换(Physical Redeem) * 方法:`POST` * 路径:`/api/v1/playx/physical/redeem` #### 请求 Body 必填: * `item_id`:商品 ID(要求 `mall_item.type=PHYSICAL` 且 `status=1`) * `receiver_name`:收货人 * `receiver_phone`:收货电话 * `receiver_address`:收货地址 鉴权: * `session_id` 或 `user_id` #### 返回(成功) * `msg`:`Redeem success` * `data`:`null` #### 示例 ```bash curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'item_id=200' \ --data-urlencode 'receiver_name=张三' \ --data-urlencode 'receiver_phone=18800001111' \ --data-urlencode 'receiver_address=北京市朝阳区XX路XX号' \ --data-urlencode 'session_id=7b1c....' ``` 响应(示例): ```json { "code": 1, "msg": "Redeem success", "time": 1712345687, "data": null } ``` --- ### 3.7 提现申请(Withdraw Apply) * 方法:`POST` * 路径:`/api/v1/playx/withdraw/apply` #### 请求 Body 必填: * `item_id`:商品 ID(要求 `mall_item.type=WITHDRAW` 且 `status=1`) 鉴权: * `session_id` 或 `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/playx/withdraw/apply' \ -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.8 订单列表 * 方法:`GET` * 路径:`/api/v1/playx/orders` #### 请求参数 * `session_id` 或 `user_id` #### 返回(成功 data) * `list`:订单列表(最多 100 条),并包含关联的 `mallItem`(关系对象) #### 示例 请求: ```bash curl -G 'http://localhost:1818/api/v1/playx/orders' --data-urlencode 'session_id=7b1c....' ``` 响应(示例,简化): ```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.9 同步额度(可选) 当前代码未实现并未注册路由:`/api/v1/playx/sync-limit`。