diff --git a/.env-example b/.env-example index 296d056..7b69736 100644 --- a/.env-example +++ b/.env-example @@ -34,6 +34,8 @@ AGENT_AUTH_JWT_SECRET= PLAYX_SESSION_EXPIRE_SECONDS=3600 # verifyToken 是否仅本地联调(false=走对方远程校验) PLAYX_VERIFY_TOKEN_LOCAL_ONLY=false +# verifyToken 对方接口路径 +PLAYX_TOKEN_VERIFY_URL=/api/v1/auth/verify-token # verifyToken 仅本地联调时的默认用户ID PLAYX_VERIFY_TOKEN_LOCAL_DEFAULT_USER_ID=testmyr # verifyToken 仅本地联调时的默认用户名 diff --git a/app/api/controller/v1/Playx.php b/app/api/controller/v1/Playx.php index 779f7e8..f9ca347 100644 --- a/app/api/controller/v1/Playx.php +++ b/app/api/controller/v1/Playx.php @@ -413,6 +413,10 @@ class Playx extends Api $baseUrl = config('playx.angpow_import.base_url', ''); $verifyUrl = config('playx.api.token_verify_url', '/api/v1/auth/verify-token'); + $verifyPath = ltrim(strval($verifyUrl), '/'); + if ($verifyPath === '') { + return $this->error(__('PlayX API not configured')); + } if ($baseUrl === '') { return $this->error(__('PlayX API not configured')); } @@ -425,7 +429,7 @@ class Playx extends Api } $requestId = 'mall_' . uniqid(); - $requestDate = gmdate('Y-m-d H:i:s'); + $requestDate = strval(time()); $signatureInput = 'merchant_code=' . $merchantCode . '&request_date=' . $requestDate . '&request_id=' . $requestId @@ -453,18 +457,12 @@ class Playx extends Api 'request_id' => $requestId, 'token' => $token, ]; - $res = $client->post($verifyUrl, [ + $res = $client->post($verifyPath, [ 'headers' => $headers, 'json' => $payload, ]); - if ($res->getStatusCode() === 405) { - $res = $client->get($verifyUrl, [ - 'headers' => $headers, - 'query' => $payload, - ]); - } - $code = $res->getStatusCode(); $data = json_decode(strval($res->getBody()), true); + $code = $res->getStatusCode(); if ($code !== 200 || empty($data['user_id'])) { $remoteMsg = ''; if (is_array($data)) { diff --git a/config/playx.php b/config/playx.php index 132c41f..f35e0af 100644 --- a/config/playx.php +++ b/config/playx.php @@ -32,7 +32,7 @@ return [ 'api' => [ 'base_url' => strval(env('PLAYX_API_BASE_URL', '')), 'secret_key' => strval(env('PLAYX_API_SECRET_KEY', '')), - 'token_verify_url' => '/api/v1/auth/verify-token', + 'token_verify_url' => strval(env('PLAYX_TOKEN_VERIFY_URL', '/api/v1/auth/verify-token')), 'bonus_grant_url' => '/api/v1/bonus/grant', 'balance_credit_url' => '/api/v1/balance/credit', 'transaction_status_url' => '/api/v1/transaction/status', diff --git a/docs/PlayX-对接文档(积分商城).md b/docs/PlayX-对接文档(积分商城).md index 45c41c9..2dd5d93 100644 --- a/docs/PlayX-对接文档(积分商城).md +++ b/docs/PlayX-对接文档(积分商城).md @@ -71,15 +71,15 @@ flowchart LR 1. 用户在 playX 内打开积分商城入口(iframe)。 2. playX 前端通过 postMessage 将 **playX 下发的 token**(及必要上下文)传给商城 H5。 -3. 商城 H5 调用商城后端 **`POST /api/v1/mall/verifyToken`**,由商城向 playX 的 **Token Verification API**(`playX.api.base_url` + `playX.api.token_verify_url`)发起校验。 -4. **前提**:配置 **`playX.verify_token_local_only = false`**,且 **`playX.api.base_url`** 已配置为可访问的 playX 基地址。 +3. 商城 H5 调用商城后端 **`POST /api/v1/mall/verifyToken`**(前端只传 `token`),由商城后端向 playX 的 **Token Verification API**(`PLAYX_ANGPOW_IMPORT_BASE_URL` + `PLAYX_TOKEN_VERIFY_URL`)发起校验。 +4. **前提**:配置 **`playX.verify_token_local_only = false`**,且 **`PLAYX_ANGPOW_IMPORT_BASE_URL`** 已配置为可访问的 playX 基地址。 5. playX 返回 **`user_id`、`username`**(及可选会话过期时间等)。 6. 商城写入 **`mall_playx_session`**(`session_id` + 上述 `user_id`/`username` + 过期时间),后续 H5 可用 **`session_id`** 或 **`token`(商城临时 token,见模式 B)** 调用资产/领取等接口。 幂等与安全: - H5 **不要**把 playX 的 `user_id` 当作唯一可信凭据直传下单;**以 token 换 session** 或由商城签发 token 的流程为准。 -- playX 侧 Token Verification API 的鉴权/签名(若有)按双方约定(可参考《playX-接口文档》§2.1)。 +- playX 侧 Token Verification API 的鉴权/签名由商城后端完成(`merchant_code/request_date/request_id` + `X-Request-Signature`);H5 不参与签名计算。 #### 4.1.3 模式 B:本地 / 无 playX 环境(商城自校验,不请求 playX) @@ -212,19 +212,29 @@ flowchart LR ### 5.2 Mall → playX:Token Verification API - **目的**:商城后端校验 token/session,获取可信 `user_id` 与 `username`。 -- **Method/Path(示例占位)**:`POST /api/v1/auth/verify-token` +- **Method**:`POST` +- **Base URL**:`PLAYX_ANGPOW_IMPORT_BASE_URL`(当前联调:`https://plx-uat2.ttwd3.com/en-my/mall`) +- **Path**:`/api/v1/auth/verify-token` +- **完整 URL**:`{PLAYX_ANGPOW_IMPORT_BASE_URL}/api/v1/auth/verify-token` +- **签名 Header**:`X-Request-Signature`(`Base64(HMAC-SHA1(canonical, key))`) +- **签名明文 canonical**:`merchant_code={merchant_code}&request_date={request_date}&request_id={request_id}&token={token}` +- **request_date 格式**:Unix 时间戳字符串(秒) -请求字段说明(建议): +请求字段说明(当前实现): | 字段名 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | +| `merchant_code` | String | 是 | 商户编码,默认 `plx`。 | +| `request_date` | String | 是 | 请求时间戳(秒)。 | | `request_id` | String | 是 | 商城系统生成的唯一请求流水号。 | -| `token` 或 `session` | String | 是 | 从带有商城的 Iframe `postMessage` 接收到的用户加密登录散列或临时会话凭证。 | +| `token` | String | 是 | 从带有商城的 Iframe `postMessage` 接收到的用户登录凭证。 | 请求示例: ```json { + "merchant_code": "plx", + "request_date": "1700000000", "request_id": "mall_20260319_9f1b6d", "token": "eyJhbGciOi..." } diff --git a/docs/PlayX-调用积分商城接口说明.md b/docs/PlayX-调用积分商城接口说明.md index 98f6568..b5e4d03 100644 --- a/docs/PlayX-调用积分商城接口说明.md +++ b/docs/PlayX-调用积分商城接口说明.md @@ -271,159 +271,356 @@ curl -X POST 'https://{商城域名}/api/v1/mall/dailyPush' \ --- -## 5. 其他接口一览(摘要) +## 5. 其他接口一览(前端联调版) -> 下列均为 **BuildAdmin 通用 `code/msg/time/data` 结构**;成功时 `code=1`。 +> 下列接口统一返回 `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`。 -用于将 **playX token**(或本地联调 token)换 **商城 `session_id`**。 +**前端入参约定(重要)**: +- 前端/客户端调用本接口时,**只需要传 `token`**(或兼容传 `session`)。 +- `merchant_code`、`request_date`、`request_id`、`X-Request-Signature` 由商城后端生成并带给 playX,前端无需参与签名。 +- 若仅传 `token` 仍校验失败,通常是 token 本身无效/过期或对方网关路径未放通,并非前端少传字段。 -| 参数位置 | 名称 | 说明 | -|----------|------|------| -| POST/GET | `token` 或 `session` | playX 或商城 token | +**请求示例:** +```json +{ + "token": "eyJhbGciOi..." +} +``` -**说明:** 若 `playX.verify_token_local_only=true`(默认),商城**仅本地校验** token,不请求 playX 远程接口;远程模式需配置 `PLAYX_API_BASE_URL` 等。 - -**成功 `data` 示例:** - -| 字段 | 说明 | -|------|------| -| `session_id` | 后续接口可带此字段 | -| `user_id` | playX 用户 ID 或映射后的标识 | -| `username` | 用户名 | -| `token_expire_at` | ISO8601 过期时间 | - ---- +**成功响应示例:** +```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` +**用途**:查询当前用户积分资产。 -查询积分资产;需 **§4** 身份。 +**请求示例:** +```http +GET /api/v1/mall/assets?session_id=fc7f3e3f0d0f4cb29f66e4c8fbab4f66 +``` -**成功 `data`:** - -| 字段 | 说明 | -|------|------| -| `locked_points` | 待领取积分 | -| `available_points` | 可用积分 | -| `today_limit` | 今日可领取上限 | -| `today_claimed` | 今日已领取 | -| `withdrawable_cash` | 可提现现金(由积分×配置比例换算,保留小数) | - ---- +**成功响应示例:** +```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` +**用途**:领取积分(幂等)。 -领取积分;需 **§4** 身份。 +**请求示例:** +```json +{ + "session_id": "fc7f3e3f0d0f4cb29f66e4c8fbab4f66", + "claim_request_id": "claim_20260430_0001" +} +``` -| 参数 | 必填 | 说明 | -|------|------|------| -| `claim_request_id` | 是 | 幂等请求号 | - -**成功 `data`:** 与资产结构一致(含 `locked_points`、`available_points`、`today_limit`、`today_claimed`、`withdrawable_cash` 等)。 - ---- +**成功响应示例:** +```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 +``` -| 参数 | 必填 | 说明 | -|------|------|------| -| `type` | 否 | `BONUS` / `PHYSICAL` / `WITHDRAW`,筛选类型 | - -**成功 `data`:** `{ "list": [ ... ] }` - ---- +**成功响应示例:** +```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` +**用途**:兑换红利商品。 -红利兑换;需 **§4** 身份。 +**请求示例:** +```json +{ + "session_id": "fc7f3e3f0d0f4cb29f66e4c8fbab4f66", + "item_id": 101 +} +``` -| 参数 | 必填 | 说明 | -|------|------|------| -| `item_id` | 是 | 商品 ID | - -**成功 `data`:** 含 `order_id`、`status`(如 `PENDING`)等。 - ---- +**成功响应示例:** +```json +{ + "code": 1, + "msg": "Success", + "time": 1777533000, + "data": { + "order_id": "ORD202604300001", + "status": "PENDING" + } +} +``` ### 5.6 `POST /api/v1/mall/physicalRedeem` +**用途**:兑换实物商品(需要地址)。 -实物兑换;需 **§4** 身份。 +**请求示例:** +```json +{ + "session_id": "fc7f3e3f0d0f4cb29f66e4c8fbab4f66", + "item_id": 202, + "address_id": 12 +} +``` -| 参数 | 必填 | 说明 | -|------|------|------| -| `item_id` | 是 | 实物商品 ID | -| `address_id` | 是 | `mall_address.id`(当前用户下地址);订单保存 `mall_address_id` 与地址快照 | - ---- +**成功响应示例:** +```json +{ + "code": 1, + "msg": "Success", + "time": 1777533000, + "data": { + "order_id": "ORD202604300002", + "status": "PENDING" + } +} +``` ### 5.7 `POST /api/v1/mall/withdrawApply` +**用途**:发起提现档位兑换申请。 -提现类兑换申请;需 **§4** 身份。 +**请求示例:** +```json +{ + "session_id": "fc7f3e3f0d0f4cb29f66e4c8fbab4f66", + "item_id": 303 +} +``` -| 参数 | 必填 | 说明 | -|------|------|------| -| `item_id` | 是 | 提现档位商品 ID | - ---- +**成功响应示例:** +```json +{ + "code": 1, + "msg": "Success", + "time": 1777533000, + "data": { + "order_id": "ORD202604300003", + "status": "PENDING" + } +} +``` ### 5.8 `GET /api/v1/mall/orders` +**用途**:查询当前用户订单列表。 -订单列表;需 **§4** 身份。 +**请求示例:** +```http +GET /api/v1/mall/orders?session_id=fc7f3e3f0d0f4cb29f66e4c8fbab4f66 +``` -**成功 `data`:** `{ "list": [ ... ] }`(含关联商品等,以实际返回为准)。 +**成功响应示例:** +```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` +**用途**:查询积分变动日志。 -### 5.9 收货地址(`mall_address`) +**请求示例:** +```http +GET /api/v1/mall/pointsLogs?session_id=fc7f3e3f0d0f4cb29f66e4c8fbab4f66 +``` -> 下列接口均需携带 **§4 身份参数**(`session_id` / `token` / `user_id` 之一)。 +**成功响应示例:** +```json +{ + "code": 1, + "msg": "Success", + "time": 1777533000, + "data": { + "list": [ + { + "id": 9001, + "change_points": 50, + "type": "CLAIM", + "remark": "daily claim" + } + ] + } +} +``` -#### 5.9.1 获取收货地址列表 -- **方法**:`GET` -- **路径**:`/api/v1/mall/addressList` +### 5.10 收货地址(`mall_address`) +**用途**:用户收货地址 CRUD。 -返回 `data.list`:地址数组(按 `default_setting` 优先,其次 id 倒序)。 +#### 5.10.1 `GET /api/v1/mall/addressList` +**请求示例:** +```http +GET /api/v1/mall/addressList?session_id=fc7f3e3f0d0f4cb29f66e4c8fbab4f66 +``` -#### 5.9.2 添加收货地址 -- **方法**:`POST` -- **路径**:`/api/v1/mall/addressAdd` +**成功响应示例:** +```json +{ + "code": 1, + "msg": "Success", + "time": 1777533000, + "data": { + "list": [ + { + "id": 12, + "receiver_name": "Tom", + "phone": "0123456789", + "detail_address": "KLCC", + "default_setting": 1 + } + ] + } +} +``` -Body(表单或 JSON 均可,建议 JSON): -| 字段 | 必填 | 说明 | -|------|------|------| -| `receiver_name` | 是 | 收货人 | -| `phone` | 是 | 联系电话 | -| `region` | 是 | 地区(可传数组或逗号分隔字符串) | -| `detail_address` | 是 | 详细地址(短文本) | -| `default_setting` | 否 | `1` 设为默认地址;`0` 或不传为非默认 | +#### 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 +} +``` -成功返回:`data.id` 为新地址 id。 +**成功响应示例:** +```json +{ + "code": 1, + "msg": "Success", + "time": 1777533000, + "data": { + "id": 13 + } +} +``` -#### 5.9.3 修改收货地址(含设置默认) -- **方法**:`POST` -- **路径**:`/api/v1/mall/addressEdit` +#### 5.10.3 `POST /api/v1/mall/addressEdit` +**请求示例:** +```json +{ + "session_id": "fc7f3e3f0d0f4cb29f66e4c8fbab4f66", + "id": 13, + "detail_address": "Tower B, 10F", + "default_setting": 1 +} +``` -Body: -| 字段 | 必填 | 说明 | -|------|------|------| -| `id` | 是 | 地址 id | -| `receiver_name/phone/region/detail_address/default_setting` | 否 | 需要修改的字段(只更新传入项) | +**成功响应示例:** +```json +{ + "code": 1, + "msg": "Success", + "time": 1777533000, + "data": null +} +``` -当 `default_setting=1`:会自动把该用户其他地址的 `default_setting` 置为 0。 +#### 5.10.4 `POST /api/v1/mall/addressDelete` +**请求示例:** +```json +{ + "session_id": "fc7f3e3f0d0f4cb29f66e4c8fbab4f66", + "id": 13 +} +``` -#### 5.9.4 删除收货地址 -- **方法**:`POST` -- **路径**:`/api/v1/mall/addressDelete` - -Body: -| 字段 | 必填 | 说明 | -|------|------|------| -| `id` | 是 | 地址 id | - -若删除的是默认地址:服务端会将剩余地址里 id 最大的一条自动设为默认(若存在)。 +**成功响应示例:** +```json +{ + "code": 1, + "msg": "Success", + "time": 1777533000, + "data": null +} +``` --- @@ -433,7 +630,10 @@ Body: |-----------------|------| | `PLAYX_DAILY_PUSH_SECRET` | 非空则 Daily Push 必须带合法 HMAC 头 | | `PLAYX_VERIFY_TOKEN_LOCAL_ONLY` | 为 true 时 verifyToken 不请求 playX 远程 | -| `PLAYX_API_BASE_URL` | 商城调用 playX 接口时使用(与「playX 调商城」方向相反) | +| `PLAYX_ANGPOW_IMPORT_BASE_URL` | 远程 `verify-token` 基础地址(当前联调可含站点前缀路径) | +| `PLAYX_TOKEN_VERIFY_URL` | 远程 `verify-token` 路径(默认 `/api/v1/auth/verify-token`) | +| `PLAYX_ANGPOW_MERCHANT_CODE` | `verify-token` 请求体 `merchant_code` | +| `PLAYX_ANGPOW_IMPORT_AUTH_KEY` | `verify-token` 签名密钥(HMAC-SHA1,Base64 输出) | ---