Files
webman-buildadmin-mall/docs/积分商城-PlayX对接实施方案.md

684 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 积分商城 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`:商品 IDBONUS
* 鉴权:`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`:商品 IDPHYSICAL
* `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`:商品 IDWITHDRAW
* 鉴权:`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