# 积分商城 PlayX 对接实施方案 > 基于《积分商城-内部对接与流程说明.md》和《PlayX-对接文档(积分商城).md》整理,结合当前项目结构给出具体落地方案。 --- ## 一、接口创建 ### 1.1 商城需对外提供的接口(PlayX 调用商城) #### Daily Push API 接收 PlayX 每日 T+1 数据推送。 * 方法:`POST` * 路径:`/api/v1/mall/dailyPush` ##### 请求(Header) 当配置了 `playx.daily_push_secret`(Daily Push 签名校验)时,需要携带: * `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)` ##### 请求(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): * `data.request_id`:原样返回 * `data.accepted`:`true` * `data.deduped`:是否幂等命中(`false`=首次入库,`true`=重复推送) * `data.message`:首次为 `ok`,重复为 `duplicate input` ##### 示例 无签名校验(`PLAYX_DAILY_PUSH_SECRET` 为空): ```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" } } ``` --- ### 1.2 商城需调用的 PlayX 接口(外部,由 PlayX 提供) 以下为商城侧调用(由 PlayX 提供)。 #### 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`(必选) * `username`(可选) * `token_expire_at`(可选,能被 `strtotime` 解析) ##### 示例 ```json { "request_id": "mall_abc123", "token": "PLAYX_TOKEN_XXX" } ``` ```json { "user_id": "U123", "username": "demo_user", "token_expire_at": "2026-04-01T12:00:00Z" } ``` #### 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 | 是 | 订单幂等键:`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` | ##### 示例(请求) ```json { "request_id": "mall_bonus_abc123", "externalTransactionId": "BONUS_ORD2026....", "user_id": "U123", "amount": 10.0, "rewardName": "每日红利", "category": "daily", "categoryTitle": "每日", "multiplier": 1 } ``` ##### 返回(期望) 商城侧以 `data.status` 判断: * `status = "accepted"`:读取 `playx_transaction_id` * 否则:读取 `message` 写入 `fail_reason` 示例(accepted): ```json { "status": "accepted", "playx_transaction_id": "PX_TX_001" } ``` 示例(reject): ```json { "status": "rejected", "message": "insufficient balance" } ``` #### 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 | 是 | 订单幂等键:`external_transaction_id` | | `user_id` | string | 是 | PlayX 用户 ID | | `amount` | number | 是 | 订单金额:`MallPlayxOrder.amount` | | `multiplier` | int | 是 | `MallPlayxOrder.multiplier` | ##### 返回(期望) 与 Bonus Grant 一致: * 成功:`status="accepted"` + `playx_transaction_id` * 失败:`message` 写入 `fail_reason` ##### 示例 请求(示意,由商城侧发起,实际 URL 以配置为准): ```json { "request_id": "mall_withdraw_abc123", "externalTransactionId": "WITHDRAW_ORD2026....", "user_id": "U123", "amount": 100.0, "multiplier": 1 } ``` 响应(accepted): ```json { "status": "accepted", "playx_transaction_id": "PX_TX_002" } ``` 响应(rejected): ```json { "status": "rejected", "message": "insufficient balance" } ``` #### Transaction Status Query API(交易终态查询) * 方法:`GET` * URL:`${playx.api.base_url}${playx.api.transaction_status_url}`(默认 `/api/v1/transaction/status`) ##### Query * `externalTransactionId`:订单幂等键 ##### 示例(请求) ```bash curl -G '${playx.api.base_url}/api/v1/transaction/status' \ --data-urlencode 'externalTransactionId=BONUS_ORD2026....' ``` ##### 返回(期望) 商城读取 `data.status`: * `COMPLETED`:设置订单 `status=COMPLETED` * `FAILED` 或 `REJECTED`:设置订单 `status=REJECTED`、`grant_status=FAILED_FINAL`,并退回积分;失败信息取 `data.message` 示例(completed): ```json { "status": "COMPLETED" } ``` 示例(failed): ```json { "status": "FAILED", "message": "grant rejected by PlayX" } ``` --- ### 1.3 商城内部 API(供 H5 前端调用) #### Token 验证 * 方法:`POST` * 路径:`/api/v1/mall/verifyToken` 请求(Body): * `token`(必填,优先读取) * 兼容:`session`(当 `token` 为空时当作 token 使用) 成功返回(data): * `session_id` * `user_id` * `username` * `token_expire_at`(ISO 时间字符串) 示例: ```bash curl -X POST 'http://localhost:1818/api/v1/mall/verifyToken' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'token=PLAYX_TOKEN_XXX' ``` #### 用户资产 * 方法:`GET` * 路径:`/api/v1/mall/assets` 请求参数(二选一): * `session_id`(优先):从 `mall_playx_session` 查 user_id(并校验过期) * `user_id`:直接使用(兼容) 成功返回(data): * `locked_points` * `available_points` * `today_limit` * `today_claimed` * `withdrawable_cash`(`available_points * points_to_cash_ratio`,保留 2 位) ##### 示例 ```bash curl -G 'http://localhost:1818/api/v1/mall/assets' --data-urlencode 'session_id=7b1c....' ``` ```json { "code": 1, "msg": "", "data": { "locked_points": 100, "available_points": 50, "today_limit": 200, "today_claimed": 80, "withdrawable_cash": 5.2 } } ``` #### 领取(Claim) * 方法:`POST` * 路径:`/api/v1/mall/claim` 请求: * `claim_request_id`:幂等键(string,必填且唯一) * 鉴权:`session_id` 或 `user_id` 成功返回(data):与资产接口一致(`locked_points/available_points/today_limit/today_claimed/withdrawable_cash`) ##### 示例 (首次领取成功,可能返回 `msg=Claim success`;若幂等重复,`msg` 可能为空) ```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 'session_id=7b1c....' ``` ```json { "code": 1, "msg": "Claim success", "data": { "locked_points": 60, "available_points": 90, "today_limit": 200, "today_claimed": 120, "withdrawable_cash": 9.0 } } ``` #### 商品列表 * 方法:`GET` * 路径:`/api/v1/mall/items` 请求(可选): * `type=BONUS|PHYSICAL|WITHDRAW` 成功返回(data): * `list`:`mall_item` 列表(包含 `amount/multiplier/category/category_title` 等字段) ##### 示例 ```bash curl -G 'http://localhost:1818/api/v1/mall/items' --data-urlencode 'type=BONUS' ``` ```json { "code": 1, "msg": "", "data": { "list": [ { "id": 123, "title": "每日红利", "type": 1, "score": 100, "amount": 10.0, "multiplier": 1, "category": "daily", "category_title": "每日" } ] } } ``` #### 红利兑换(Bonus Redeem) * 方法:`POST` * 路径:`/api/v1/mall/bonusRedeem` 请求: * `item_id`:商品 ID(BONUS) * 鉴权:`session_id` 或 `user_id` 成功返回(data): * `order_id` * `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", "data": { "order_id": 456, "status": "PENDING" } } ``` #### 实物兑换(Physical Redeem) * 方法:`POST` * 路径:`/api/v1/mall/physicalRedeem` 请求: * `item_id`:商品 ID(PHYSICAL) * `address_id`:`mall_address.id`(当前用户资产下地址;订单写入 `mall_address_id` 与收货快照) * 鉴权:`session_id` 或 `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", "data": null } ``` #### 提现申请(Withdraw Apply) * 方法:`POST` * 路径:`/api/v1/mall/withdrawApply` 请求: * `item_id`:商品 ID(WITHDRAW) * 鉴权:`session_id` 或 `user_id` 成功返回(data): * `order_id` * `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", "data": { "order_id": 789, "status": "PENDING" } } ``` #### 订单列表 * 方法:`GET` * 路径:`/api/v1/mall/orders` 请求: * `session_id` 或 `user_id` 成功返回(data): * `list`:订单列表(最多 100 条,包含关联的 `mallItem`) ##### 示例 ```bash curl -G 'http://localhost:1818/api/v1/mall/orders' --data-urlencode 'session_id=7b1c....' ``` ```json { "code": 1, "msg": "", "data": { "list": [ { "id": 456, "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 } } ] } } ``` #### 同步额度 当前代码未实现并未注册路由:`/api/v1/mall/syncLimit`。 如需补齐,请在接口设计阶段新增对应实现与 PlayX API 对接。 --- ## 二、后台修改 ### 2.1 商品管理(mall_item) - **类型**:需与文档一致 - `1` = BONUS(红利) - `2` = PHYSICAL(实物) - `3` = WITHDRAW(提现档位) - **新增字段**(红利/提现档位): - `amount`:现金面值(元) - `multiplier`:流水倍数 - `category`:红利业务类别(如 daily) - `category_title`:类别展示名 - **库存**:实物需 `stock`;红利/提现可不限制或按业务配置 ### 2.2 订单管理(统一订单表) - **订单类型**:BONUS / PHYSICAL / WITHDRAW - **订单状态**:PENDING / COMPLETED / SHIPPED / REJECTED - **实物订单**: - 发货:录入物流公司、单号 → `SHIPPED` - 驳回:录入驳回原因 → `REJECTED`,自动退回积分 - **红利/提现订单**: - 展示 `external_transaction_id`、`playx_transaction_id`、推送playx - 手动重试:仅对 `FAILED_RETRYABLE` 状态,记录 `retry_request_id`、操作者、原因 ### 2.3 用户资产与人工调账 - **用户资产**:按 `user_id` 展示 `locked_points`、`available_points`、`today_limit`、`today_claimed` - **人工调账**:针对 T+1 推送异常或客诉,支持对 `locked_points`、`available_points` 手动加减,并记录审计日志 ### 2.4 每日推送数据 - 后台可查看 `mall_playx_daily_push` 表数据,按 `user_id`、`date` 查询,便于排查异常 --- ## 三、数据库修改 ### 3.1 用户资产表(改造 mall_player 或新建) **方案 A:改造 mall_player** - 将 `user_id` 作为主键(PlayX 的 user_id)或与现有 id 并存 - 新增字段: - `locked_points`(待领取积分) - `available_points`(可用积分) - `today_limit`(今日可领取上限) - `today_claimed`(今日已领取) - `today_limit_date`(今日上限所属日期,用于每日重置) **方案 B:新建 mall_playx_user_asset** - `user_id`(PlayX 用户 ID,主键或唯一) - `username`(冗余展示) - `locked_points`、`available_points`、`today_limit`、`today_claimed`、`today_limit_date` - `create_time`、`update_time` --- ### 3.2 每日推送数据表(新建) **表名**:`mall_playx_daily_push` | 字段 | 类型 | 说明 | |------|------|------| | id | int | 主键 | | user_id | varchar(64) | 玩家 ID | | date | date | 业务日期 | | username | varchar(100) | 展示名 | | yesterday_win_loss_net | decimal(15,2) | 昨日净输赢 | | yesterday_total_deposit | decimal(15,2) | 昨日总充值 | | lifetime_total_deposit | decimal(15,2) | 历史总充值 | | lifetime_total_withdraw | decimal(15,2) | 历史总提现 | | create_time | bigint | 创建时间 | **唯一索引**:`(user_id, date)` 幂等去重 --- ### 3.3 领取记录表(幂等) **表名**:`mall_playx_claim_log` | 字段 | 类型 | 说明 | |------|------|------| | id | int | 主键 | | claim_request_id | varchar(64) | 幂等键,唯一 | | user_id | varchar(64) | 用户 ID | | claimed_amount | int | 领取积分 | | create_time | bigint | 创建时间 | **唯一索引**:`claim_request_id` --- ### 3.4 统一订单表(改造或新建) **表名**:`mall_playx_order`(或统一改造 mall_pints_order / mall_redemption_order) | 字段 | 类型 | 说明 | |------|------|----------------------------------------------------------------------| | id | int | 主键 | | user_id | varchar(64) | 用户 ID | | type | enum | BONUS / PHYSICAL / WITHDRAW | | status | enum | PENDING / COMPLETED / SHIPPED / REJECTED | | mall_item_id | int | 商品 ID | | points_cost | int | 消耗积分 | | amount | decimal(15,2) | 现金面值(红利/提现) | | multiplier | int | 流水倍数 | | external_transaction_id | varchar(64) | 订单号 | | playx_transaction_id | varchar(64) | PlayX 流水号 | | grant_status | enum | NOT_SENT / SENT_PENDING / ACCEPTED / FAILED_RETRYABLE / FAILED_FINAL | | fail_reason | text | 失败原因 | | retry_count | int | 重试次数 | | reject_reason | varchar(255) | 驳回原因(实物) | | shipping_company | varchar(50) | 物流公司 | | shipping_no | varchar(64) | 物流单号 | | mall_address_id | int unsigned, NULL | 实物兑换所选 `mall_address.id`(快照仍见 `receiver_*`) | | receiver_name | varchar(50) | 收货人 | | receiver_phone | varchar(20) | 收货电话 | | receiver_address | text | 收货地址 | | create_time | bigint | 创建时间 | | update_time | bigint | 更新时间 | **索引**:`user_id`、`external_transaction_id`、`type`、`status` --- ### 3.5 商品表(mall_item)扩展 新增字段(若不存在): | 字段 | 类型 | 说明 | |------|------|------| | amount | decimal(15,2) | 现金面值(红利/提现档位) | | multiplier | int | 流水倍数 | | category | varchar(32) | 红利业务类别 | | category_title | varchar(64) | 类别展示名 | --- ## 四、业务规则 ### 4.1 计算规则(需配置) - **返还比例**:`新增保障金 = ABS(yesterday_win_loss_net) * 返还比例`(仅 `yesterday_win_loss_net < 0` 时) - **解锁比例**:`今日可领取上限 = yesterday_total_deposit * 解锁比例` - **提现折算**:积分 → 现金(如 10 分 = 1 元),用于前端展示 ### 4.2 每日重置 - `today_claimed` 与 `today_limit` 按业务日重置(`today_limit_date` 变化时) ### 4.3 发放重试 - 自动重试:1min / 5min / 15min,最多 3 次,使用同一 `externalTransactionId` - 仅对 `NOT_SENT`、`FAILED_RETRYABLE` 重试 - 收到 `accepted` 后不再重试,改轮询交易终态查询 API --- ## 五、实施顺序建议 1. **数据库**:新增迁移(`mall_playx_daily_push`、`mall_playx_claim_log`、`mall_playx_order`),扩展 `mall_item`、`mall_player`(或新建资产表) 2. **模型**:`MallPlayxDailyPush`、`MallPlayxClaimLog`、`MallPlayxOrder`、扩展 `MallItem`、`MallPlayer` 3. **接口**:Daily Push API(含签名校验)→ Token 验证 → 资产/领取 → 商品列表 → 红利/实物/提现 → 订单列表 4. **后台**:商品扩展、订单管理(含发货/驳回/重试)、人工调账、每日推送数据查看 5. **定时任务**:轮询交易终态、自动重试失败发放 --- ## 六、待确认事项 - PlayX 提供的 **Token Verification API**、**Bonus Grant API**、**Balance Credit API**、**交易终态查询 API** 的 URL、鉴权方式、字段最终表 - `date` 的时区定义(如 UTC+8) - 返还比例、解锁比例、提现折算的具体数值 - 是否启用「同步额度」功能(需 PlayX 提供对应 API)