# 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`。 **前端入参约定(重要)**: - 前端/客户端调用本接口时,**只需要传 `token`**(或兼容传 `session`)。 - `merchant_code`、`request_date`、`request_id`、`X-Request-Signature` 由商城后端生成并带给 playX,前端无需参与签名。 - 若仅传 `token` 仍校验失败,通常是 token 本身无效/过期或对方网关路径未放通,并非前端少传字段。 **请求示例:** ```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` | 远程 `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 输出) | --- ## 7. 版本与变更 - 文档与仓库代码同步维护;接口路径以 `config/route.php` 为准。 - 若后续升级鉴权策略(例如叠加 JWT),以部署环境变量与最新文档为准。