## 1. 文档目的与读者

本文件为 **内部使用** 的完整说明，用于：

- 梳理积分商城与 playX 之间的 **全量业务流程**（含当前选型与备选方案）。
- 统一后台实现口径（资产计算、订单状态机、幂等、重试、对账）。
- 为后续版本扩展（实时 webhook、同步按钮、playX 拉取模式）预留空间。

对外给 playX 的精简版请参考：`playX-对接文档（积分商城）.md`。

## 2. 当前选型概览（V1.0）

- **主键标识**：`user_id`（贯穿每日推送、资产、订单、发放接口）。
- **集成方式**：
  - 前端：商城 H5 以 Iframe 嵌入 playX，postMessage 传 token/session。
  - 后端：商城独立服务，通过 REST API 与 playX 通讯。
- **数据来源**：
  - 资产池：仅使用 **每日 Cron 推送（T+1）** 的历史输赢与充值数据。
  - 不引入实时充值/流水 webhook 作为资产来源。
- **发放模式**：
  - 商城在红利/提现下单后，**直接调用 playX 发放接口**（Bonus Grant / Balance Credit）。
  - playX 侧每 10 分钟 Cron 执行 5.9 adjustment / 最终入账。
- **幂等责任分工（方案 A，与对外文档一致）**：
  - **playX**：对 Bonus Grant / Balance Credit 按 **`externalTransactionId` 严格幂等**——同一单号重复请求不得产生第二笔发放；须返回与首次受理一致或可识别的幂等结果（具体 HTTP 体字段由联调约定）。
  - **商城**：为每笔红利/提现生成**全局唯一** `externalTransactionId`；收到 HTTP 200 且 `status="accepted"` 后**不再向发放接口重放**；对网络超时等“未知是否受理”场景，可在有限次内重试**同一** `externalTransactionId`，依赖 playX 幂等保证不双发（见第 6 章）。

## 3. 角色、系统与对象

### 3.1 角色

- **会员**：在 playX 内通过 Iframe 使用积分商城。
- **运营/客服**：使用商城后台管理商品、订单、调账。
- **playX 平台**：数据源与权益发放执行方。

### 3.2 主要对象与前端看板展示映射

- **目前可提现（现金）**：
  - 前端展示专用虚拟字段，由可用积分按固定比例折算（如：10 分 = 1 元），用于给玩家呈现直观价值，非底层独立资产。
- **待领取积分 (LockedPoints)**：
  - **定义**：基于昨日玩家亏损转化来的“保障池”，未领取前不可消费。
  - **数据源**：playX 每日推送的 `Yesterday Player Win loss`（取绝对值 × 返还比例）。
- **可用积分 (AvailablePoints)**：
  - **定义**：玩家当前拥有的、可立即抵扣兑换和提现的真实积分资产。
  - **交互消耗**：所有兑换、提现操作均扣减该字段。
- **今日可领取上限 (TodayLimit)**：
  - **定义**：限制玩家当日最多能“挽回”多少积分。
  - **数据源**：playX 每日推送的 `Yesterday Total Deposit`（昨日总充值 × 解锁比例）。
- **今日已领取 (TodayClaimed)**：
  - **定义**：记录玩家当日累计已领取的积分规模，辅助校验上限，每日重置。
- **订单 (Order)**：
  - 类型：BONUS / PHYSICAL / WITHDRAW
  - 状态：PENDING / COMPLETED / SHIPPED / REJECTED

## 4. 端到端流程（内部视角）

### 4.1 登录与会话建立

1. 会员在 playX 点击“积分商城”入口。
2. 父页面加载 Iframe，商城前端进入“连接中/鉴权中”态。
3. playX 前端通过 postMessage 发送：
   - `token` 或 session 标识。
4. 商城前端将 token 传给后端，后端调用 playX 的 **Token Verification API** 验证身份：
   - **请求参数**：
     - `token`：接收到的用户会话凭证。
     - `request_id`：系统发起的鉴权校验流水号。
   - **响应参数**：
     - `user_id`：解析出的玩家在平台的唯一标识主键。
     - `username`：仅用于前端展示的昵称。
     - `token_expire_at`：用于判断并在濒临过期时触发换取新 Token（无感续期）。
5. 商城根据 `user_id` 加载本地资产与订单数据，渲染首页。

**安全与会话续期要点**：

- **安全拦截**：不信任前端传入的 `user_id`，必须通过 Token Verification API 获取可信 `user_id`。
- **Token 续期**：如果用户在商城停留过久导致 Token 过期（接口返回 `401 / INVALID_TOKEN`），商城前端需通过 `postMessage` 通知 playX 父页面重新派发新 Token 以实现静默续期；若无法自动续期，则需弹窗引导用户重新进入商城。

### 4.2 每日 T+1 数据推送与资产入池

数据来源：playX 每日 Cron。

- **交互字段说明（T+1 核心输入）**：
  - `date`：归属业务定日（用于限定该批数据的生效周期）。
  - `user_id`：玩家平台唯一标识主键。
  - `yesterday_win_loss_net`：昨日净输赢金额（核心数据源：负数绝对值作为计算新增保障积分类的基准）。
  - `yesterday_total_deposit`：昨日总充值（核心数据源：作为计算今日领取该积分上限的阈值基准）。
  - `lifetime_total_deposit` / `lifetime_total_withdraw` / `username`：非运算强制性字段，用于冗余显示或阶层判断预留。

商城处理逻辑：

1. 按 `user_id + date` 幂等入库，避免重复处理。（**注：需与 playX 明确 `date` 的时区定义，如 UTC+8 等**）。
2. 根据业务规则计算：
   - `新增保障金 = ABS(yesterday_win_loss_net) * 返还比例`（仅当 yesterday_win_loss_net < 0 时产生）。
   - `今日可领取上限 = yesterday_total_deposit * 解锁比例`。**注意：今日上限每日独立计算，不结转至次日。**
3. 将新增保障金累加到 `LockedPoints`，更新 `TodayLimit`。

**数据修正机制**：

- T+1 推送不支持覆盖更新（冲正）。如果 playX 上游数据算错导致推送有误，商城在二次推送时会触发去重拦截（`deduped`）。此类异常数据统一由**商城后台人工调账**处理。

### 4.3 领取流程（从待挽回转化为可用）

触发：会员在首页核心看板上查看领取进度（基于 `昨日充值` 计算），如果进度符合预期，点击“立即领取”。

- **前端交互**：
  - 判断逻辑：若 `昨日充值`（即 TodayLimit）均不足导致上限不够，按钮可能置灰并提示“昨日存款不足，无法全额领取”。
  - 操作反馈：点击后从底部呼出二次确认层（“将待领取积分划转为可用积分后，即可兑换或提现。确定领取？”）。
  - 确认层点击“确定”后发出后端请求。
- **后端校验条件**：
  - `LockedPoints > 0`
  - `TodayLimit - TodayClaimed > 0`
- **后端计算与执行**：
  - `canClaim = min(LockedPoints, TodayLimit - TodayClaimed)`
  - **原子更新数据库**：
    - `LockedPoints -= canClaim`
    - `AvailablePoints += canClaim`
    - `TodayClaimed += canClaim`
- **防重与幂等**：
  - 接口入参包含 `claim_request_id`，同一 `claim_request_id` 重复提交不重复扣减。
- **前端成功响应**：
  - 后端处理完成后，前端弹出“领取成功，积分已到账”状态框。
  - **即时刷新**看板中的 `待领取积分` 与 `可用积分` 与 `目前可提现现金`。
- **积分有效期说明**：
  - V1.0 版本积分无有效期限制，`LockedPoints` 和 `AvailablePoints` 永久有效并持续累加，后续视财务成本压力再引入年底清零/周期过期机制。

### 4.4 红利兑换（BONUS）

1. 会员在前端选择红利商品（配置来自后台商品管理）。
2. 前端展示二次确认弹层（金额、流水倍数、说明），**并需在 UI 上显式提示会员：“发放预计需等待约 10 分钟内到账”**。
3. 提交后，前端进入“兑换中”类似状态，**不可直接提示“兑换成功”**，随后商城后端执行：
   - 校验积分余额是否足够。
   - 校验商品状态（上架、库存）。
   - 校验订单状态（防重复提交）。
4. 创建订单：
   - `type = BONUS`
   - `status = PENDING`
   - `user_id`
   - 商品信息 & 消耗积分
5. 原子扣减 `AvailablePoints`。
6. 生成 `externalTransactionId`（例如 `BONUS_ORD{订单号}`）。
7. 调用 playX Bonus Grant API（核心透传字段，详见外部对接文档）：
   - `externalTransactionId`：本单派发流水号（**防重**：用于要求下游强制幂等拦截）。
   - `user_id`：玩家标示。
   - `amount`：红利发放切实的现金面值。
   - `multiplier`：这笔款项后续提款的流水约束倍数。
   - `rewardName`：展示给玩家此笔红利来源名称。
   - `category`：便于平台统计对账的红利业务大类。
8. playX 返回：
   - 若 HTTP 200 且 `status = "accepted"`：
     - 记录 `playx_transaction_id`。
     - 订单保持 PENDING（等待对方系统最终发放）。
     - **发起请求动作不再重试**。但商城侧需定时调用 playX 提供的 **交易状态查询 API** 轮询确认该订单最终结果，成功则转 `COMPLETED`，失败则转 `REJECTED` 并退分。
   - 否则：
     - 记录失败原因。
     - 进入“可重试队列”（自动/人工重试，见第 6 章）。

### 4.5 实物兑换（PHYSICAL）

1. 会员选择实物商品并填写收货信息。
2. 商城后端：
   - 校验库存/积分。
   - 创建 PENDING 订单，扣减 `AvailablePoints`。
3. 后台处理：
   - 发货：录入物流公司与单号，状态 → SHIPPED。**(可选：调用 playX Inbox API 给用户发送发货通知站内信)**。
   - 驳回：录入驳回原因，状态 → REJECTED，并退回积分。**(可选：调用 playX Inbox API 告知用户驳回原因)**。

### 4.6 提现回平台余额（WITHDRAW 操作逻辑）

本流程旨在将兑换所得的虚拟积分，按照规定“提现”为充入 playX 平台账户的真实金额。

1. **前端选择与展示**：
   - 会员在首页或“提现到平台”类别列表中选择特定提现档位（如：提现 100 元，需要 1000 积分，1倍流水要求）。
   - 点击“提现”按钮后，前端唤出底部二次确认弹层。
   - 弹层内容展示所选档位**所需消耗的积分值**以及对应的**流水要求倍数**，等待二次确认提交。
   - （**前端同时需在弹层或说明中向用户提示：提现申请预计约 10 分钟在平台入账**）。
2. **后端可用性校验**：
   - 用户确认后，前端请求后端。商城后端比较当前用户的 `AvailablePoints` 是否 `>= item.points`。如果不足，阻断流程并返回前端 `积分不足` 的报错浮层。
3. **资产扣减与订单落库**：
   - 原子扣减数据库内该会员的 `AvailablePoints`。
   - 创建 WITHDRAW 暂挂订单（状态 `PENDING`），记录该单提现对应的 `amount` 与 `multiplier`。
   - 生成外部交易单号 `externalTransactionId`（如 `WITHDRAW_ORD{订单号}`）。
4. **调用 playX API 发放（核心参数解析）**：
   - 商城发包调用 playX 的 Balance Credit API：
     - `externalTransactionId`：本提现申请的订单号（**提现防重唯一拦截键**）。
     - `user_id`：发起提现的玩家 ID。
     - `amount`：要充入平台余额池的真金面值。
     - `multiplier/turnover_rule`：该笔提现资金入账后锁定的打码流水倍数条件。
5. **异步等待终态与 UI 回显**：
   - 收到 API `accepted` 响应后，商城将不间断返回前端“提交成功，预计 10 分钟内处理”。
   - 商城内部保持该订单为 PENDING，并进入定时轮询状态，监控 playX 10 分钟 Cron 执行后的“最终业务发货结果”，闭环完成后才转入 COMPLETED 或对失败按规则退分。

## 5. 扩展与备选方案（暂不对外）

本章为 **扩展设计/备选方案**，当前版本不对 playX 提出实现要求，只在内部保留。

### 5.1 实时充值 webhook（备选）

用途：让“今日可领取上限”不仅依赖 T+1 数据，还能实时响应充值行为。

示例设计：

- playX 在充值成功后，调用商城的充值 webhook：
  - 字段：`user_id`、`amount`、`transaction_id`、`occurred_at`。
- 商城：
  - 以 `transaction_id` 幂等入库。
  - 更新“当日实时充值统计”，供风控或前端展示使用。

当前状态：

- V1.0 不启用此对接；所有领取逻辑仅基于每日推送（T+1）。

### 5.2 外部积分来源 webhook（任务/轮盘）

用途：把 playX 任务、幸运轮盘等活动产出的积分汇总到商城。

示例设计：

- playX 调用 webhook：
  - 字段：`user_id`、`points`、`source`、`transaction_id`。
- 商城：
  - 以 `transaction_id` 幂等增加 `AvailablePoints` 或某个“活动积分池”。

当前状态：

- V1.0 不做接入；防止资产口径复杂化。

### 5.3 “同步额度”交互逻辑（手动拉取实时充值）

**功能目的**：由于积分商城的基础计算依赖的是 T+1 每日推送，如果不做任何补充，今日充值的玩家将无法即刻提升“今日可领取上限（TodayLimit）”。因此原型中设置了“同步额度”按钮，作为手动拉取实施更新的一种补救路径。

**前端交互逻辑**：
- 用户在首页面板点击“同步额度”次要操作按钮。
- 前端短暂 loading，调用后端接口拉取后，弹出轻量级反馈：“已同步最新额度”。
- 页面数据看板数字重新渲染刷新。

**后端业务逻辑选型推荐**：

- **方案 A（建议，商城做拉取请求）**：
  - 会员点击同步按钮，商城后端拦截并向 playX 系统调用一个**“查询今日实时余额/重算存款 API”**。
  - 获取到最新存款后，累加或覆盖当前的 `TodayLimit`。
- **方案 B（playX 控制权）**：
  - 点击后，通过 iframe 的 `postMessage` 向父级 playX 窗口发送同步指令。
  - playX 在自身域内统计今日所有游戏存款流水进行汇总归集（甚至一键转账入主钱包的操作）。
  - 处理完成后 playX 主动请求积分商城的`更新 webhook`来给 `TodayLimit` 加额，最后前端获取成功事件刷新。
- **当前定案落地方向**：
  - 根据原型要求，本功能**必须落地**。推荐使用方案 A。商城侧需准备一个接受通知的 API，或 playX 需要支持实时提供玩家当日总存款的只读 API，以供点击拉取。对于 V1.0 对外文档里若不打算实现，需与 playX 进一步交涉决定是否阉割掉此按钮。

### 5.4 发放模式备选：playX 定时拉取

备选方案：

- 由 playX 每 10 分钟调用商城查询接口，拉取待发放订单列表，然后自行发放。

需要新增：

- 商城提供“待发放订单列表”接口（分页、过滤、幂等标记）。
- 双方需要就“已拉取但未发放”、“重复拉取”等边界做严格定义。

当前选型：

- 考虑到复杂度与 playX 当前发放系统形态，最终选择 **商城主动调用 playX 发放 API** 的模式，拉取模式仅保留在内部文档中作为备选。

## 6. 幂等、重试与状态机（内部实现口径）

### 6.1 幂等键

- 每日推送：`user_id + date`。
- 充值/外部积分 webhook（如启用）：`transaction_id`。
- 领取：`claim_request_id`。
- 红利/提现发放：`externalTransactionId`。

### 6.2 发放请求状态机（商城内部）

针对每个订单（BONUS/WITHDRAW），在商城内部维护推送playX状态，例如：

- `NOT_SENT`：未发送给 playX。
- `SENT_PENDING`：已发送，等待 playX 响应。
- `ACCEPTED`：收到 HTTP 200 且 `status = "accepted"`。
- `FAILED_RETRYABLE`：失败且可重试（如超时、上游错误）。
- `FAILED_FINAL`：最终失败（达到重试上限或 playX 返回不可恢复错误）。

关键规则：

- 只有在 `NOT_SENT` 或 `FAILED_RETRYABLE` 状态下才允许“再次发送”。
- 一旦进入 `ACCEPTED`，不得再发请求（自动或人工）。
- 订单业务状态（PENDING/COMPLETED/REJECTED）与推送playX状态之间要有清晰映射：
  - `ACCEPTED` + 对账确认成功 → `COMPLETED`。
  - `FAILED_FINAL` → `REJECTED`（并退积分）。

### 6.3 重试策略（内部）与防重底线

- **前提**：playX 已承诺按 **`externalTransactionId` 严格幂等**（方案 A）。在此前提下，读超时后使用**同一** `externalTransactionId` 的有限次自动重试是安全的。
- 自动重试：
  - 仅针对网络错误、Read Timeout（读超时）、`PLAYX_UPSTREAM_ERROR` 等，且**尚未收到** HTTP 200 + `status="accepted"`。
  - 建议间隔：1min / 5min / 15min，最多 3 次；**每次重试必须使用原订单的同一** `externalTransactionId`，不得生成新单号冒充新单。
- 人工重试：
  - 仅允许在 `FAILED_RETRYABLE` 状态下触发。
  - 每次需记录 `retry_request_id`、操作者、原因。若自动重试耗尽仍失败，可先通过 **交易终态查询 API** 核对再决定是否人工干预。
- 绝不重试场景：
  - 发放请求已明确收到 `status = "accepted"`（后续只走 5.5 轮询与客诉流程，不向发放接口重放）。
  - 明确业务拒绝类错误（如参数非法、规则不满足）。

## 7. 对账与问题排查

- 对账来源：
  - 商城订单表（含 externalTransactionId、playx_transaction_id）。
  - playX 提供的对账/流水查询（如有）。
- 常见问题场景：
  - 商城显示 REJECTED 但会员反馈已收到红利：需检查是否在“发放后回滚积分”链路出错。
  - 商城显示 PENDING 时间过长：需排查 playX 侧 10 分钟 Cron 是否正常。

## 8. 与对外文档的关系

- 本文档：覆盖“能想到的所有对接与流程设计”，供产品、后端、运营、商务内部统一认识。
- `playX-对接文档（积分商城）.md`：
  - 仅暴露 playX V1.0 必须提供/实现的部分。
  - 承诺最小闭环，不在主文中提及实时 webhook、同步按钮、拉取模式。
  - **同步额度等能力**若产品仍要落地，以本文 **5.3** 与商务/playX 结论为准；对外文档不写不代表产品一定不做。

