## 0. 交付说明（给 playX）

- **交付物**：本文件（接口清单 + 业务流程 + 联调验收清单）。
- **建议联调顺序**：Token 验证（远程 playX 或本地 `verify_token_local_only`）→ 每日推送 → 领取 → 红利发放 → 提现入账 → 实物后台处理。
- **约定**：接口 URL、字段最终表、签名细节以 playX 提供的最终口径为准；本文档负责把流程、幂等、重试与最小字段集合先对齐。

## 1. 文档目的与范围

本文档用于 playX 与积分商城（Points Mall）联调对接。范围仅包含：

- 前端：playX 以内嵌 Iframe 打开商城 H5，使用 postMessage 传递 token/session。
- 后端：商城后端独立部署；与 playX 后端通过 REST API 通讯。
- 数据同步：仅 playX 每日 Cron 推送（T+1）玩家数据到商城，用于计算“待领取积分/今日可领取上限”。
- 发放方式：商城在红利兑换/提现（回平台余额）下单后，直接调用 playX API 发放/入账；playX 侧每 10 分钟 Cron 执行 5.9 adjustment/最终入账。

不在本文档范围内：

- 任何实时 webhook（充值、外部积分、流水等）。
- 会员端“同步额度/同步流水”按钮触发的对接链路。

## 2. 系统边界与调用方向

```mermaid
flowchart LR
  playXFrontend["playXFrontend"] -->|"postMessage(token/session)"| MallFrontend["MallFrontend(Iframe)"]
  MallFrontend -->|"API(商城后端)"| MallBackend["MallBackend"]
  MallBackend -->|"TokenVerificationAPI"| playXBackend["playXBackend"]
  playXBackend -->|"DailyPushAPI(T+1)"| MallBackend
  MallBackend -->|"BonusGrantAPI/BalanceCreditAPI"| playXBackend
```

> 当 **`playX.verify_token_local_only=true`** 时，「Token 验证」一步在商城内完成，**不经过** `playXBackend` 的 Token Verification API；详见 **§4.1**。

## 3. 关键业务对象与状态机

### 3.1 资产口径（最小集合）

- **LockedPoints（待领取积分）**：由 playX 每日推送的“昨日输赢净额”在商城端按规则计算得到，未领取前不可消费。
- **AvailablePoints（可用积分）**：领取后可用于兑换/提现的积分余额。
- **TodayLimit（今日可领取上限）**：由 playX 每日推送的“昨日总存款”按规则计算得到。
- **TodayClaimed（今日已领取）**：当日累计领取量（用于进度条与上限控制）。

### 3.2 订单类型

- **BONUS**：红利兑换
- **PHYSICAL**：实物兑换
- **WITHDRAW**：提现回平台余额（非现金出款）

### 3.3 统一订单状态

- **PENDING（处理中）**：订单已创建，等待发放/审核/发货等后续处理
- **COMPLETED（已发放）**：红利到账或提现入账完成
- **SHIPPED（已发货）**：实物已发货，包含物流公司与单号
- **REJECTED（已驳回）**：失败或人工拒绝；积分需退回（退回规则见 6.2）

## 4. 端到端流程（6 条）

### 4.1 登录鉴权（Iframe + token）

> **接口与字段细节**以代码为准，完整说明见同目录《playX-接口文档.md》（§3 H5、§3.2 `temLogin`、§3.3 `verifyToken`）。

#### 4.1.1 身份与数据模型（商城侧）

- **商城用户**：表 `mall_user`（H5 临时登录、后台创建等均落此表）。
- **playX 资产扩展**：表 `mall_playx_user_asset`，与 `mall_user` **一对一**（`mall_user_id`、`playx_user_id` 均唯一）。
- **业务侧用户标识**：对外接口中的 `user_id`（字符串）在多数场景下即 **`playx_user_id`**（playX 玩家 ID）。  
  - 若用户仅通过商城 **临时登录** 进入、尚无 playX 正式 ID，商城会生成占位 ID，形如 **`mall_{mall_user.id}`**，与每日推送中的真实 `user_id` 区分（避免与纯数字 ID 混淆）。
- **H5 调业务接口时**：服务端内部统一解析为 **`mall_user.id`**，再查资产与订单（解析规则见《playX-接口文档》§3.1）。

#### 4.1.2 模式 A：联调 playX（生产/预发，远程校验 token）

1. 用户在 playX 内打开积分商城入口（iframe）。
2. playX 前端通过 postMessage 将 **playX 下发的 token**（及必要上下文）传给商城 H5。
3. 商城 H5 调用商城后端 **`POST /api/v1/mall/verifyToken`**（前端只传 `token`），由商城后端向 **Token Verification API** 发起校验。
4. **前提**：配置 **`playX.verify_token_local_only = false`**，且 **`PLAYX_TOKEN_VERIFY_URL`** 已配置：
   - **完整 `https://...` URL**（如回调）：商城 `POST` 该地址；JSON Body 含 **`merchant_code`、`request_date`（Unix 秒）、`request_id`、`token`**；**`X-Request-Signature`** 与 angpow-imports **同源算法**（密钥 **`PLAYX_ANGPOW_IMPORT_AUTH_KEY`**），签名字符串为 **`merchant_code=...&request_date=...&request_id=...&token=...`**；无需 `PLAYX_ANGPOW_IMPORT_BASE_URL`。
   - **相对路径**（如 `/api/v1/auth/verify-token`）：需同时配置 **`PLAYX_ANGPOW_IMPORT_BASE_URL`**，Body 使用 **`request_date`**，并按商户约定附带签名等。
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_VERIFY_URL` 为**完整 https URL**：Body 含 **`merchant_code`、`request_date`、`request_id`、`token`**；`X-Request-Signature` 为 angpow 同源 HMAC，明文 **`merchant_code=...&request_date=...&request_id=...&token=...`**；H5 不参与签名。
- 若为**相对路径 + 基地址**商户网关：鉴权/签名由商城后端完成（`merchant_code/request_date/request_id` + `X-Request-Signature` 等）；H5 不参与签名计算。

#### 4.1.3 模式 B：本地 / 无 playX 环境（商城自校验，不请求 playX）

用于开发、联调前自测、或 playX 接口未就绪时：

1. 配置 **`playX.verify_token_local_only = true`**（环境变量 **`PLAYX_VERIFY_TOKEN_LOCAL_ONLY`**，默认可为开启，以项目 `config/playx.php` 为准）。
2. 此时 **`/api/v1/mall/verifyToken` 不会访问 playX**，仅在商城内校验 **商城临时 token**（token 表类型 **`muser`**，由下方 `temLogin` 签发）。
3. 调用 **`GET/POST /api/v1/temLogin?username=...`**（需 **`buildadmin.agent_auth.temp_login_enable = true`**）：不存在则创建 **`mall_user`**，并保证存在 **`mall_playx_user_asset`**（含 `playx_user_id`，默认 **`mall_{id}`**），返回 **`userInfo.token`**、**`playx_user_id`**、**`expires_in`** 等。
4. 再用该 token 调用 **`verifyToken`** 可得到 **`session_id`**，与模式 A 一样供后续接口使用；或直接带 **`token` / `ba-token`** 调资产等接口（见《playX-接口文档》§3.1）。

#### 4.1.4 会话续期与前端约定

- **会话续期**：玩家停留时间较长时，若商城 API 返回 token/session 失效（如 401），H5 可通过 postMessage 请 playX 父页面 **重新派发 playX token**（模式 A）；模式 B 下可重新 **`temLogin`** 或走 **`/api/common/refreshToken`**（`muser-refresh`）换取新 access token。
- 具体错误码与 Header（如 `ba-token`）以前端与《playX-接口文档》为准。

### 4.2 每日 T+1 入池（playX → 商城）

1. playX 在每日固定时间向商城调用 **Daily Push API**，推送昨日玩家数据。（**注：请确认并约定好 `date` 字段对应的具体时区边界，如以 UTC+8 为准**）。
2. 商城按 `user_id + date` 幂等去重入库。由于不支持通过重复推送做数据修正，**若 playX 发现个别账单算错了，请联系商城运营在后台进行人工调账处理**，勿重复推送。
3. 商城计算：
   - 新增保障金（待领取积分增量）
   - 今日可领取上限
4. 会员次日进入商城时，可在首页看到更新后的 LockedPoints 与 TodayLimit。

### 4.3 领取流程（Locked → Available）

1. 会员在首页点击“领取”。
2. 商城后端校验：LockedPoints > 0，且 TodayLimit - TodayClaimed > 0。
3. 商城计算 `canClaim = min(LockedPoints, TodayLimit - TodayClaimed)`，并原子更新：
   - LockedPoints -= canClaim
   - AvailablePoints += canClaim
   - TodayClaimed += canClaim
4. 返回最新资产，前端刷新。

幂等：

- 领取操作建议使用 `claim_request_id`（由前端生成或后端生成返回）实现幂等，避免重复点击导致重复领取。

### 4.4 红利兑换（商城 → playX 发放）

1. 会员在“红利”商品点击兑换并确认（**为避免客诉，商城前端会提示会员：红利发放预计在此后约 10 分钟内入账，请耐心等待**）。
2. 商城创建 BONUS 订单（PENDING），并校验/扣减可用积分（原子扣减）。
3. 商城调用 playX **Bonus Grant API**，传递红利发放信息（字段见 5.3）。
4. 若 playX API 返回初步排队接收成功（HTTP 200 且 `status="accepted"`）：
   - 商城订单保持 PENDING（等待 playX 侧 10 分钟 Cron 最终发放/入账）。
   - 记录 `playx_transaction_id`（或外部流水号）用于后续追踪。
   - **商城后端将通过调用 playX 的 “交易状态查询 API”（见 5.5）来轮询获取最终结果**，最终确认为成功后，商城订单才会流转闭环为 COMPLETED。
5. 若 playX API 返回失败：
   - 订单保持 PENDING，并记录失败原因与下一次可重试时间
   - 支持后台“手动重试”（见 6.3）
   - 若经过 N 次重试仍失败或确认 playX 侧不可达成：订单转 REJECTED 并退回积分（见 6.2）

### 4.5 实物兑换（商城后台人工处理）

1. 会员选择实物并填写收货信息（姓名/电话/地址）。
2. 商城创建 PHYSICAL 订单（PENDING），并原子扣减可用积分。
3. 后台运营：
   - 发货：录入物流公司与单号 → 状态 SHIPPED
   - 驳回：录入原因 → 状态 REJECTED → 自动退回积分

### 4.6 提现回平台余额（商城 → playX 入账）

1. 会员在“提现到平台余额”商品点击提现并确认（**前端同样需向用户提示约 10 分钟入账预期**）。
2. 商城创建 WITHDRAW 订单（PENDING），并原子扣减可用积分。
3. 商城调用 playX **Balance Credit API**（或同一发放接口的提现模式），传入入账信息。
4. 若 playX API 返回初步排队接收成功（HTTP 200 且 `status="accepted"`）：
   - 商城订单保持 PENDING（等待 playX 侧 10 分钟 Cron 最终入账）。
   - 记录 `playx_transaction_id`（或外部流水号）用于后续追踪。
   - **商城后端通过「交易状态查询 API」（见 5.5）轮询获取终态**，确认成功后订单才流转为 COMPLETED。
5. 若 playX API 返回失败（非 200 或 `status` 非 `accepted`）：失败处理同 4.4。

## 5. 接口清单（按调用方向）

> 说明：以下为接口“结构与字段清单”。具体 URL、Header、签名算法、错误码需 playX 提供或双方确认后固化。

### 5.1 playX → Mall：Daily Push API（每日推送）

- **目的**：推送昨日玩家数据，用于 T+1 计算入池与领取上限。
- **幂等键**：`user_id + date`（date 建议为 playX 业务日）
- **Method/Path（建议）**：`POST /api/v1/mall/dailyPush`

请求字段说明（最小集合，来自现有资料）：

| 字段名 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `request_id` | String | 是 | 请求的唯一网关流水号，商城端用于日志追踪和外层防重。 |
| `date` | String | 是 | 数据归属的业务日期（如 `"2026-03-18"`），用于限定该批数据的生效周期。 |
| `user_id` | String | 是 | 玩家在 playX 的唯一标识 ID，在此商城体系中以此作为核心绑定主键。 |
| `username` | String | 否 | 玩家展示名，仅用于后台日志人工可读性或冗余展示，不作业务主键。 |
| `lifetime_total_deposit` | Decimal | 否 | 玩家历史总充值（如有需要用于玩家 VIP 分层，当前传值保留即可）。 |
| `lifetime_total_withdraw` | Decimal | 否 | 玩家历史总提现（储备字段）。 |
| `yesterday_win_loss_net` | Decimal | 是 | 昨日净输赢金额（如果玩家亏损，应为负数）。**注：务必是已扣除返点、红利、奖励、推荐佣金、VIP Bonus 的税后净额**，严格代表玩家的真实净负盈利。 |
| `yesterday_total_deposit` | Decimal | 是 | 昨日玩家总充值金额，积分商城专门用此字段来计算“今日可领取上限（TodayLimit）”。 |

请求示例：

```json
{
  "request_id": "px_20260319_000001",
  "date": "2026-03-18",
  "user_id": "U123",
  "username": "demo_user_01",
  "lifetime_total_deposit": 5000.0,
  "lifetime_total_withdraw": 2000.0,
  "yesterday_win_loss_net": -120.5,
  "yesterday_total_deposit": 50.0
}
```

响应字段说明（建议）：

| 字段名 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `request_id` | String | 是 | 完全透传原请求的 `request_id`，便于双向日志匹配追踪。 |
| `accepted` | Boolean | 是 | `true` 标识商城已成功接收并解析了该批数据。 |
| `deduped` | Boolean | 是 | 若 `true`，标识该条数据因 `user_id + date` 已存在而被商城系统幂等静默丢弃（去重）。 |
| `message` | String | 否 | 成功或失败的补充说明（如 `"ok"` 或 `"duplicate input"` 等异常提示）。 |

响应示例：

```json
{
  "request_id": "px_20260319_000001",
  "accepted": true,
  "deduped": false,
  "message": "ok"
}
```

### 5.2 Mall → playX：Token Verification API

- **目的**：商城后端校验 token，获取可信 `user_id` 与 `username`。
- **Method**：`POST`
- **Content-Type**：`application/json`

#### 5.2.1 回调网关（推荐当前联调）

- **完整 URL**：由环境变量 **`PLAYX_TOKEN_VERIFY_URL`** 填完整地址，例如：  
  `https://callback-mallsys.superior3.net/callback/api/mallsys/plx/auth/verify-token`
- **请求 Header**：与 **angpow-imports** 相同风格，仅 **`Content-Type: application/json`**、**`X-Request-Signature`**（`Base64(HMAC-SHA1(canonical, PLAYX_ANGPOW_IMPORT_AUTH_KEY))`，密钥解析与 angpow 一致：hex / base64 / 明文）。
- **签名明文 canonical**：`merchant_code={merchant_code}&request_date={request_date}&request_id={request_id}&token={token}`（`request_date` 为 Unix 秒字符串，与 Body 一致）。
- **请求 Body**（回调网关实际校验；在文档 `request_id`+`token` 基础上增加对端要求的 **`merchant_code`、`request_date`**）：

| 字段名 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `merchant_code` | String | 是 | 与 `PLAYX_ANGPOW_MERCHANT_CODE` 一致。 |
| `request_date` | String | 是 | Unix 秒时间戳字符串，须与参与签名一致。 |
| `request_id` | String | 是 | 商城生成的请求追踪号。 |
| `token` | String | 是 | 前端传入的 playX 临时凭证。 |

请求示例：

```json
{
  "merchant_code": "plx",
  "request_date": "1700000000",
  "request_id": "mall_20260319_9f1b6d",
  "token": "eyJhbGciOi..."
}
```

#### 5.2.2 商户网关（相对路径 + HMAC）

- **Base URL**：`PLAYX_ANGPOW_IMPORT_BASE_URL`
- **Path**：`PLAYX_TOKEN_VERIFY_URL`（相对路径，如 `/api/v1/auth/verify-token`）
- **完整 URL**：`{PLAYX_ANGPOW_IMPORT_BASE_URL}{PLAYX_TOKEN_VERIFY_URL}`
- **签名 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 | 是 | 商户编码。 |
| `request_date` | String | 是 | 请求时间戳（秒）。 |
| `request_id` | String | 是 | 商城系统生成的唯一请求流水号。 |
| `token` | String | 是 | 用户登录凭证。 |

```json
{
  "merchant_code": "plx",
  "request_date": "1700000000",
  "request_id": "mall_20260319_9f1b6d",
  "token": "eyJhbGciOi..."
}
```

响应字段说明（建议）：

| 字段名 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `request_id` | String | 是 | 透传请求时的 `request_id`。 |
| `user_id` | String | 是 | 该凭证解密后对应的、在 playX 平台具有唯一性的玩家专属 ID。 |
| `username` | String | 否 | 该玩家显示名，用于加载商城的界面头部“欢迎：xxx”渲染。 |
| `token_expire_at` | String | 否 | Token 的物理过期时间（如 ISO8601），用于商城前端预判是否到了需要执行无感续期重置的底线时间。 |

响应示例：

```json
{
  "request_id": "mall_20260319_9f1b6d",
  "user_id": "U123",
  "username": "demo_user_01",
  "token_expire_at": "2026-03-19T10:12:00Z"
}
```

### 5.3 Mall → playX：Bonus Grant API（红利发放）

来自 playX 现有字段清单（待 playX 确认最终口径）：

请求字段说明（待 playX 确认最终口径）：

| 字段名 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `request_id` | String | 是 | 商城发起的 HTTP 请求流水号（纯用于网关层）。 |
| `externalTransactionId` | String | 是 | **核心防重键：强制要求 playX 凭此字段做完全的幂等拦截**。这是商城侧派发红利的唯一本地订单号（如 `"BONUS_ORD001"`）。 |
| `user_id` | String | 是 | 要派发红利的玩家在 playX 的基础 ID，这需对齐每日推送。 |
| `memberLogin` | String | 否 | 玩家登录名（若当前 playX 核心接口必须传登录名，则商城会补充；若以 `user_id` 为准，此项可废弃）。 |
| `amount` | Decimal | 是 | 实际加给玩家游戏余额或红利钱包的具体现金数字。 |
| `rewardName` | String | 否 | 商城中对应的该红利商品名称，用于让用户后续在 playX 流水里看懂这笔钱从何而来。 |
| `description` | String | 否 | 系统行为备注说明（如 `"PointsMall bonus"`）。 |
| `memberInboxMessage` | String | 否 | 是否需借调此时机向玩家发送站内站群信内容提示。 |
| `category` | String | 是 | 标明该红利在游戏侧的所属业务类别的枚举代码（如 `daily`）。 |
| `categoryTitle` | String | 否 | 该红利业务类别的中文展示名称。 |
| `multiplier` (或 `turnover`) | Int | 是 | 款项入账后，玩家需完成的打码流水约束倍数（如 1 倍或 5 倍）。 |
| `startTime` / `endTime` | String | 否 | 红利生效时间窗口（起止时间，视 playX 规则传参）。 |

请求示例：

```json
{
  "request_id": "mall_bonus_20260319_000001",
  "externalTransactionId": "BONUS_ORD20260319_000001",
  "user_id": "U123",
  "memberLogin": "demo_user_01",
  "amount": 50.0,
  "rewardName": "每日回馈 50",
  "description": "PointsMall bonus redemption",
  "memberInboxMessage": "红利已提交，预计 10 分钟内到账",
  "category": "daily",
  "categoryTitle": "每日回馈",
  "multiplier": 1,
  "startTime": "2026-03-19T00:00:00Z",
  "endTime": "2026-03-19T23:59:59Z"
}
```

响应字段说明（建议）：

| 字段名 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `request_id` | String | 是 | 透传请求号。 |
| `playx_transaction_id` | String | 否 | playX 内部初创的接收入列单号或派发流水号，商城会将其归档以备应对极端客诉争议寻找记录用。 |
| `status` | String | 是 | 核心状态枚举。若为 `accepted`，表示请求成功列入 10 分钟 Cron，商城中止重试；其他值皆触发商城的补偿拦截网。 |
| `message` | String | 否 | 对入列状态的额外提示信息内容。 |

响应示例：

```json
{
  "request_id": "mall_bonus_20260319_000001",
  "playx_transaction_id": "PX_TX_778899",
  "status": "accepted",
  "message": "queued"
}
```

### 5.4 Mall → playX：Balance Credit API（提现回平台余额）

字段建议与 5.3 保持结构一致，至少包含：

请求字段说明（建议与 5.3 保持结构一致）：

| 字段名 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `request_id` | String | 是 | 商城下发的网络请求追溯号。 |
| `externalTransactionId` | String | 是 | **提现唯一单号，提现接口也需要基于此值做绝对拦截幂等功能**。 |
| `user_id` | String | 是 | 申请提现的玩家 ID。 |
| `memberLogin` | String | 否 | 玩家名（视老接口历史包袱兼容）。 |
| `amount` | Decimal | 是 | 本次提现要充入 playX 主游戏平台真金余额池的具体现金。 |
| `multiplier` (或 `turnover_rule`) | Int | 是 | 本真金提现入账后的硬性流水锁定要求倍数限制。 |
| `description` | String | 否 | 日志源记录说明（如 `"PointsMall withdraw"`）。 |

请求示例：

```json
{
  "request_id": "mall_withdraw_20260319_000001",
  "externalTransactionId": "WITHDRAW_ORD20260319_000002",
  "user_id": "U123",
  "memberLogin": "demo_user_01",
  "amount": 100.0,
  "multiplier": 1,
  "description": "PointsMall withdraw to playX balance"
}
```

响应说明与 5.3 (Bonus Grant API) 保持一致，主要接收 `status="accepted"` 作为暂挂确认。

```json
{
  "request_id": "mall_withdraw_20260319_000001",
  "playx_transaction_id": "PX_TX_889900",
  "status": "accepted",
  "message": "queued"
}
```

### 5.5 Mall → playX：Transaction Status Query API（交易终态查询）

- **目的**：红利/提现申请经 playX 接收后（即返回 `accepted` 后）可能处于排队发放下款状态（如 10 分钟 Cron）。商城将通过此接口查询最终业务结果，用于闭环商城自身的 PENDING 订单。
- **Method/Path（预留）**：`GET /api/v1/transaction/status`
- **传参方式**：使用 **Query String** 传递查询主键（若 playX 更倾向 POST，可改为 `POST` + JSON body，但需在联调前双方定稿一种即可）。

示例：

- `GET /api/v1/transaction/status?externalTransactionId=WITHDRAW_ORD20260319_000002`
- `GET /api/v1/transaction/status?playx_transaction_id=PX_TX_889900`（与 `externalTransactionId` 二选一，推荐优先 `externalTransactionId`）

**轮询建议（商城侧）**：首次调用可在入队成功后约 1 分钟开始；之后间隔约 **60 秒** 查询一次，直至 `status` 为 `COMPLETED` 或 `FAILED`，或累计轮询达到约 **15～20 分钟**（与 10 分钟 Cron 留足余量）仍未终态则告警并转人工跟进。

请求字段说明（建议选其一作主键）：

| 字段名 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `externalTransactionId` | String | 是* | 商城之前提报请求时创建挂钩的原始提现单号，推荐此查维优先。 |
| `playx_transaction_id` | String | 否 | 如果之前排队响应抛出了内部派发流水，也可以持此作为二级兜底查询条件。（二选一必填） |

响应字段说明（建议）：

| 字段名 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `status` | String | 是 | 该笔资产调拨定时任务执行的彻底终态。只有两种预期终点：**`COMPLETED`**(入账成功) 或 **`FAILED`**(发放彻底阻断：如平台风控/未通过规则/账号封禁)。如果返回 `PENDING` 表示该 10 分钟 Cron 仍然没碰这笔单。 |
| `amount` | Decimal | 否 | 最后实际结算派发的精准明细金额数。 |
| `message` | String | 否 | 若拦截至 `FAILED` 终态，该字段负责说明 playX 端驳回的业务层原因，便于商城后端登记审计并自动回退积分。 |

## 6. 一致性、幂等与退回规则

### 6.1 幂等原则

- **每日推送**：以 `user_id + date` 去重，重复推送不得导致重复入池。
- **兑换/提现交易**：以 `externalTransactionId` 幂等（商城生成并传给 playX）。
- **领取**：以 `claim_request_id` 幂等，避免重复领取。

### 6.2 退积分规则（建议统一）

- **红利/提现**：
  - playX API 调用失败：订单保持 PENDING，进入可重试队列（不立即退积分，避免“退了但 playX 已受理/最终入账”的不一致）。
  - 当订单被判定为“最终失败”（例如超过最大重试次数或 playX 返回不可恢复错误）时：订单转 REJECTED，退回积分并记录原因。
- **实物**：
  - 驳回必须退回积分，并记录 `reject_reason`。

### 6.3 重试与后台操作边界

- 仅允许对 “尚未收到 `status = \"accepted\"` 响应、且可以确认未成功发放/未入账” 的订单发起重试。
- 每次重试必须生成并记录 `retry_request_id` 与操作者审计日志。
- **强制防重约定**：**playX 必须根据 `externalTransactionId` 提供严格的幂等拦截能力！**由于网络请求存在“Read Timeout（读超时）”的黑盒场景，即 playX 实际已处理但响应由于网络中断未抵达商城，商城将会发起重试保护。如果 playX 不去拦截此重发单号，将必然出现给用户发双份钱的高危资损事故。

接收成功与终态闭环判定（关键约定）：

- **第一步（接收排队）**：本系统调用发放类 API，仅当收到 HTTP 200 且 `status = "accepted"` 时，视为 playX 已接收入队成功，此时商城**绝不再对发放接口发起新的成功路径请求**。若返回非 200、或响应超时、或未能解析出明确 `accepted`：商城可对**同一** `externalTransactionId` 进行有限次重试；**playX 须对该单号严格幂等**——重复请求不得产生第二笔发放，且应返回与首次受理一致或可识别的幂等结果（如再次返回 `accepted` 或明确 `DUPLICATE_REQUEST` 等，由双方约定响应形态）。
- **第二步（确认终态闭环）**：针对已入队返回 accepted 的订单，商城将调用**“交易终态查询 API”（5.5）**验证 playX 后台的最终发放结果实现闭环更新。

默认重试策略（建议）：

- **自动重试**：对 `PLAYX_UPSTREAM_ERROR`/超时类错误，最多 3 次（间隔 1min/5min/15min）。
- **不重试**：`INVALID_SIGNATURE`、`REQUEST_EXPIRED`、`RULE_NOT_SATISFIED`、`INVALID_TOKEN`（需要修复请求或重新鉴权）。
- **人工重试**：后台按钮触发，要求输入原因并记录审计。

## 7. 安全要求（Shared Secret Key）

建议所有 playX ↔ Mall 的后端调用统一：

- Header：
  - `X-Request-Id`
  - `X-Timestamp`
  - `X-Signature`
- 签名：使用共享 `SecretKey`，对 request body + timestamp + requestId 进行 HMAC（具体算法由双方定稿）。
- 时效：timestamp 允许偏差窗口（例如 5 分钟），超出拒绝。

签名建议（可直接落地的默认）：

- `X-Signature = HMAC_SHA256(secret, canonical_string)`
- `canonical_string = X-Timestamp + \"\\n\" + X-Request-Id + \"\\n\" + HTTP_METHOD + \"\\n\" + PATH + \"\\n\" + SHA256(REQUEST_BODY_JSON)`

其中：

- `PATH` 不含域名与 querystring（例如 `/api/v1/mall/dailyPush`）。
- `REQUEST_BODY_JSON` 使用原始 request body（不做 key 排序时，需双方约定序列化方式；更推荐双方统一为“key 排序后的紧凑 JSON”）。

## 8. 错误码与可观测（建议）

最低要求：

- `INVALID_SIGNATURE`
- `REQUEST_EXPIRED`
- `INVALID_TOKEN`
- `DUPLICATE_REQUEST`
- `INSUFFICIENT_POINTS`
- `RULE_NOT_SATISFIED`
- `PLAYX_UPSTREAM_ERROR`

错误码返回结构（建议统一）：

```json
{
  "request_id": "xxx",
  "code": "PLAYX_UPSTREAM_ERROR",
  "message": "timeout",
  "retryable": true
}
```

### 8.1 幂等：同一 `externalTransactionId` 重复调用（Bonus Grant / Balance Credit）

playX 须保证：**同一** `externalTransactionId` 无论被调用多少次，**资金侧最多只入账一次**。商城在「读超时重试」或联调压测时会重复提交同一单号，响应须符合以下 **两种约定之一**（联调前择一写死，避免双方解析不一致）。

**模式 A（推荐）：再次请求仍返回 HTTP 200，且与首次受理语义一致**

- 第二次及以后请求：`status` 仍为 `"accepted"`（或文档约定的等价成功态），**不得**再次触发新的发放队列条目导致双发。
- 建议同时带回**首次**的 `playx_transaction_id`（若与首次不同，须在联调中禁止或说明兼容规则）。

```json
{
  "request_id": "mall_bonus_20260319_000099",
  "playx_transaction_id": "PX_TX_778899",
  "status": "accepted",
  "message": "duplicate externalTransactionId, already accepted"
}
```

**模式 B：显式重复错误码（HTTP 状态可与 playX 规范一致，如 200 或 409，联调前约定）**

- `code` 为 `DUPLICATE_REQUEST`（或双方统一的幂等冲突码），`retryable` 为 `false`，提示商城勿再重试发放接口、改查 5.5 终态。

```json
{
  "request_id": "mall_bonus_20260319_000099",
  "code": "DUPLICATE_REQUEST",
  "message": "externalTransactionId already processed",
  "retryable": false,
  "playx_transaction_id": "PX_TX_778899"
}
```

日志与审计：

- 每次跨系统调用必须落 `request_id`、入参摘要、响应摘要、耗时、结果码。

## 9. 联调与验收清单

### 9.1 鉴权

- token 正常/过期/无效/重复请求
- postMessage 未收到 token 的超时提示

### 9.2 每日推送

- 正常推送 1 次
- 同一 `user_id+date` 重复推送（应 dedup）
- 跨时区日期边界（按约定业务日）

### 9.3 领取

- locked=0 不可领取
- 上限不足部分领取
- 幂等：重复点击不重复加积分

### 9.4 红利/提现

- 发放接口：HTTP 200 且 `status="accepted"` 后，订单 PENDING，记录 `playx_transaction_id`，**不再对发放接口重放**（终态靠 5.5）。
- 发放接口：非 200 / 超时 / 非 `accepted`：失败原因落库，可自动或人工重试；**playX 对同一 `externalTransactionId` 须严格幂等**。
- **交易终态查询（5.5）**：按 `externalTransactionId` 查询，验证返回 `COMPLETED` / `FAILED` / `PENDING`；长时间 `PENDING` 走告警与人工。
- 幂等联调：同一 `externalTransactionId` 连续发送 2 次，playX 侧**不得重复入账**，第二次响应须符合双方约定的幂等语义。

### 9.5 实物

- 提交收货信息
- 发货录入物流单号
- 驳回退积分并展示原因

## 10. 需要 playX 提供/确认的信息清单（用于联调收口）

- **Token Verification API**：URL、请求/响应字段、错误码、token 有效期/刷新策略、是否支持 session。
- **Daily Push API**：推送时间点、时区、date 口径（业务日还是自然日）、失败重发策略、字段定义（特别是 `yesterday_win_loss_net` 的扣项范围）。
- **Bonus Grant API / Balance Credit API**：URL、鉴权签名要求、字段最终表、**确认以 `externalTransactionId` 作为拦截幂等键**，以及返回的 `playx_transaction_id` 定义与查询方式。
- **交易终态查询 API（如适用）**：提供专门供商城拉取订单最终入账结果的查询接口 URL 及返回结构。
- **发送站内信 API（如适用）**：在管理后台手动处理实物商品发货/驳回时，如需通过信箱通知用户，请提供外部触发站内信的 API 渠道。
- **枚举值配置**：请尽早提供发放接口中 `category` 等字段的固定枚举值字典，以便商城后台完成商品发货配置项的落库。

