From 3ac825f15d658ad6b1cc98be51c64a22b46d549d Mon Sep 17 00:00:00 2001
From: zhenhui <1276357500@qq.com>
Date: Tue, 21 Apr 2026 11:59:15 +0800
Subject: [PATCH] =?UTF-8?q?1.=E4=BC=98=E5=8C=96=E5=90=8E=E5=8F=B0=E9=A1=B5?=
=?UTF-8?q?=E9=9D=A2=E6=A0=B7=E5=BC=8F=202.=E4=BC=98=E5=8C=96=E7=BB=9F?=
=?UTF-8?q?=E4=B8=80=E8=AE=A2=E5=8D=95=E4=B8=AD=E7=BA=A2=E5=88=A9=E7=9A=84?=
=?UTF-8?q?=E7=8A=B6=E6=80=81=E5=92=8C=E5=A4=B1=E8=B4=A5=E5=8E=9F=E5=9B=A0?=
=?UTF-8?q?=203.=E7=A7=BB=E9=99=A4=E9=A1=B9=E7=9B=AE=E4=B8=AD=E5=86=97?=
=?UTF-8?q?=E4=BD=99=E4=BB=A3=E7=A0=81=E5=92=8C=E5=AD=97=E6=AE=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/admin/controller/mall/Order.php | 5 +-
app/admin/lang/en.php | 7 +
app/admin/lang/zh-cn.php | 7 +
app/api/controller/v1/Playx.php | 1 -
app/common/library/MallBonusGrantPush.php | 10 +-
app/common/model/MallOrder.php | 1 -
app/process/AngpowImportJobs.php | 18 +-
app/process/PlayxJobs.php | 3 +-
docs/PlayX-接口文档.md | 247 +++---------------
docs/积分商城-PlayX对接实施方案.md | 5 +-
.../table/fieldRender/failReason.vue | 75 ++++++
web/src/lang/backend/en/mall/order.ts | 1 -
web/src/lang/backend/en/mall/playxOrder.ts | 1 -
web/src/lang/backend/zh-cn/mall/claimLog.ts | 2 +-
web/src/lang/backend/zh-cn/mall/dailyPush.ts | 2 +-
web/src/lang/backend/zh-cn/mall/order.ts | 11 +-
.../lang/backend/zh-cn/mall/playxClaimLog.ts | 2 +-
.../lang/backend/zh-cn/mall/playxDailyPush.ts | 2 +-
web/src/lang/backend/zh-cn/mall/playxOrder.ts | 3 +-
.../lang/backend/zh-cn/mall/playxUserAsset.ts | 2 +-
web/src/lang/backend/zh-cn/mall/userAsset.ts | 2 +-
web/src/lang/backend/zh-cn/user/moneyLog.ts | 2 +-
web/src/utils/axios.ts | 24 +-
web/src/views/backend/mall/order/index.vue | 28 +-
.../views/backend/mall/playxOrder/index.vue | 1 -
web/types/tableRenderer.d.ts | 1 +
26 files changed, 199 insertions(+), 264 deletions(-)
create mode 100644 web/src/components/table/fieldRender/failReason.vue
diff --git a/app/admin/controller/mall/Order.php b/app/admin/controller/mall/Order.php
index 07cd4c9..7e3db33 100644
--- a/app/admin/controller/mall/Order.php
+++ b/app/admin/controller/mall/Order.php
@@ -26,7 +26,7 @@ class Order extends Backend
protected array $withJoinTable = ['mallItem'];
- protected string|array $quickSearchField = ['user_id', 'external_transaction_id', 'playx_transaction_id'];
+ protected string|array $quickSearchField = ['user_id', 'external_transaction_id'];
protected string|array $indexField = [
'id',
@@ -38,7 +38,6 @@ class Order extends Backend
'amount',
'multiplier',
'external_transaction_id',
- 'playx_transaction_id',
'grant_status',
'fail_reason',
'reject_reason',
@@ -301,7 +300,7 @@ class Order extends Backend
$result = MallBonusGrantPush::push($order);
if ($result['ok']) {
$order->grant_status = MallOrder::GRANT_ACCEPTED;
- $order->playx_transaction_id = $result['playx_transaction_id'];
+ $order->status = MallOrder::STATUS_COMPLETED;
$order->fail_reason = null;
$order->update_time = time();
$order->save();
diff --git a/app/admin/lang/en.php b/app/admin/lang/en.php
index 8e6917c..5b89ea6 100644
--- a/app/admin/lang/en.php
+++ b/app/admin/lang/en.php
@@ -100,4 +100,11 @@ return [
'PlayX API not configured' => 'PlayX API not configured',
'Current grant status cannot be manually pushed' => 'Current grant status cannot be manually pushed',
'Order status must be PENDING' => 'Order status must be PENDING',
+ 'Missing required fields' => 'Missing required fields',
+ 'Order type not PHYSICAL' => 'Order type is not physical goods',
+ 'Order type not supported' => 'Order type not supported',
+ 'Only BONUS can retry' => 'Only bonus orders can retry push',
+ 'Shipped successfully' => 'Shipped successfully',
+ 'Approved successfully' => 'Approved successfully',
+ 'Rejected successfully' => 'Rejected successfully',
];
\ No newline at end of file
diff --git a/app/admin/lang/zh-cn.php b/app/admin/lang/zh-cn.php
index ec5ee58..ac8d19a 100644
--- a/app/admin/lang/zh-cn.php
+++ b/app/admin/lang/zh-cn.php
@@ -119,4 +119,11 @@ return [
'PlayX API not configured' => 'PlayX 接口未配置',
'Current grant status cannot be manually pushed' => '当前发放状态不可手动推送',
'Order status must be PENDING' => '订单状态须为处理中',
+ 'Missing required fields' => '缺少必填项',
+ 'Order type not PHYSICAL' => '订单类型不是实物',
+ 'Order type not supported' => '订单类型不支持',
+ 'Only BONUS can retry' => '仅红利订单可重试推送',
+ 'Shipped successfully' => '发货成功',
+ 'Approved successfully' => '审核通过',
+ 'Rejected successfully' => '驳回成功',
];
\ No newline at end of file
diff --git a/app/api/controller/v1/Playx.php b/app/api/controller/v1/Playx.php
index 4d46326..a12f157 100644
--- a/app/api/controller/v1/Playx.php
+++ b/app/api/controller/v1/Playx.php
@@ -1330,7 +1330,6 @@ SQL;
]);
$data = json_decode(strval($res->getBody()), true);
if ($res->getStatusCode() === 200 && ($data['status'] ?? '') === 'accepted') {
- $order->playx_transaction_id = $data['playx_transaction_id'] ?? '';
$order->grant_status = MallOrder::GRANT_ACCEPTED;
$order->save();
} else {
diff --git a/app/common/library/MallBonusGrantPush.php b/app/common/library/MallBonusGrantPush.php
index f9d5244..defd585 100644
--- a/app/common/library/MallBonusGrantPush.php
+++ b/app/common/library/MallBonusGrantPush.php
@@ -15,7 +15,7 @@ use GuzzleHttp\Client;
final class MallBonusGrantPush
{
/**
- * @return array{ok: bool, message: string, playx_transaction_id: string}
+ * @return array{ok: bool, message: string}
*/
public static function push(MallOrder $order): array
{
@@ -24,7 +24,6 @@ final class MallBonusGrantPush
return [
'ok' => false,
'message' => 'PlayX angpow_import not configured',
- 'playx_transaction_id' => '',
];
}
@@ -36,7 +35,6 @@ final class MallBonusGrantPush
return [
'ok' => false,
'message' => 'PlayX Angpow Import API not configured',
- 'playx_transaction_id' => '',
];
}
@@ -47,7 +45,6 @@ final class MallBonusGrantPush
return [
'ok' => false,
'message' => 'User asset not found',
- 'playx_transaction_id' => '',
];
}
@@ -56,7 +53,6 @@ final class MallBonusGrantPush
return [
'ok' => false,
'message' => 'Item not found',
- 'playx_transaction_id' => '',
];
}
@@ -67,7 +63,6 @@ final class MallBonusGrantPush
return [
'ok' => false,
'message' => 'Build signature failed',
- 'playx_transaction_id' => '',
];
}
@@ -123,20 +118,17 @@ final class MallBonusGrantPush
return [
'ok' => true,
'message' => '',
- 'playx_transaction_id' => '',
];
}
return [
'ok' => false,
'message' => strval($data['message'] ?? 'PlayX angpow import not accepted'),
- 'playx_transaction_id' => '',
];
} catch (\Throwable $e) {
return [
'ok' => false,
'message' => $e->getMessage(),
- 'playx_transaction_id' => '',
];
}
}
diff --git a/app/common/model/MallOrder.php b/app/common/model/MallOrder.php
index 5f3b521..24906a4 100644
--- a/app/common/model/MallOrder.php
+++ b/app/common/model/MallOrder.php
@@ -18,7 +18,6 @@ use support\think\Model;
* @property float $amount
* @property int $multiplier
* @property string $external_transaction_id
- * @property string $playx_transaction_id
* @property string $grant_status
* @property string|null $fail_reason
* @property int $retry_count
diff --git a/app/process/AngpowImportJobs.php b/app/process/AngpowImportJobs.php
index a631adc..d7abe9a 100644
--- a/app/process/AngpowImportJobs.php
+++ b/app/process/AngpowImportJobs.php
@@ -198,6 +198,7 @@ class AngpowImportJobs
if ($code === '0' || $code === 0) {
MallOrder::whereIn('id', $orderIds)->update([
'grant_status' => MallOrder::GRANT_ACCEPTED,
+ 'status' => MallOrder::STATUS_COMPLETED,
'fail_reason' => null,
'update_time' => time(),
]);
@@ -265,6 +266,21 @@ class AngpowImportJobs
];
}
+ /**
+ * 单条失败原因压成一行,避免异常信息自带换行导致与 attempt 分段混在一起。
+ */
+ private function normalizeReasonLine(string $reason): string
+ {
+ $s = trim($reason);
+ if ($s === '') {
+ return '';
+ }
+ $s = str_replace(["\r\n", "\r", "\n"], ' ', $s);
+ $replaced = preg_replace('/\s+/', ' ', $s);
+
+ return is_string($replaced) && $replaced !== '' ? $replaced : $s;
+ }
+
private function markFailedAttempt(MallOrder $order, string $reason): void
{
// retry_count 在“准备发送”阶段已 +1;此处用当前 retry_count 作为 attempt 编号
@@ -277,7 +293,7 @@ class AngpowImportJobs
$prev = $order->fail_reason;
$prefix = 'attempt ' . $attempt . ': ';
- $line = $prefix . $reason;
+ $line = $prefix . $this->normalizeReasonLine($reason);
$newReason = $line;
if (is_string($prev) && $prev !== '') {
$newReason = $prev . "\n" . $line;
diff --git a/app/process/PlayxJobs.php b/app/process/PlayxJobs.php
index ebbb7f8..4dfcbe4 100644
--- a/app/process/PlayxJobs.php
+++ b/app/process/PlayxJobs.php
@@ -171,7 +171,8 @@ class PlayxJobs
$result = MallBonusGrantPush::push($order);
if ($result['ok']) {
$order->grant_status = MallOrder::GRANT_ACCEPTED;
- $order->playx_transaction_id = $result['playx_transaction_id'];
+ $order->status = MallOrder::STATUS_COMPLETED;
+ $order->update_time = time();
$order->save();
return;
diff --git a/docs/PlayX-接口文档.md b/docs/PlayX-接口文档.md
index c10590b..894052e 100644
--- a/docs/PlayX-接口文档.md
+++ b/docs/PlayX-接口文档.md
@@ -2,14 +2,17 @@
说明:本文档严格依据当前代码 `app/api/controller/v1/Playx.php`、`app/api/controller/v1/Auth.php`(临时登录)、`config/playx.php` 与定时任务 `app/process/PlayxJobs.php` 整理。
-三类接口分别为:
-- `积分商城 -> PlayX`(PlayX 调用商城)
-- `PlayX -> 积分商城`(商城调用 PlayX)
-- `积分商城 -> H5`(H5 调用商城)
+按调用方向分为三类(避免与历史章节标题混淆):
+
+| 方向 | 含义 | 本文位置 |
+|------|------|----------|
+| **PlayX → 积分商城** | PlayX(或上游批处理)**主动 HTTP 调用商城**开放接口 | **§1**(如 Daily Push) |
+| **积分商城 → PlayX** | 商城 Worker / 后台 **主动 HTTP 调用 PlayX / Cash Market** 提供的接口 | **不展开于本文**;交付 PlayX 的说明见 **`docs/PlayX-接口待完善清单.md`** |
+| **积分商城 → H5** | H5 / 内嵌页 **调用商城** 的会员与积分业务接口 | **§3** |
---
-## 1. 积分商城 -> PlayX(PlayX 调用商城)
+## 1. PlayX → 积分商城(外部系统调用商城开放接口)
### 1.1 Daily Push API
* 方法:`POST`
@@ -36,8 +39,8 @@
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `request_id` | string | 是 | 外部推送请求号(原样返回) |
-| `date` | string(YYYY-MM-DD) | 是 | 业务日期(入库到 `mall_playx_daily_push.date`) |
-| `user_id` | string | 是 | PlayX 用户 ID(用于幂等;入库 `mall_playx_daily_push.user_id` 等;服务端会映射/创建 `mall_user` 与 `mall_playx_user_asset`) |
+| `date` | string(YYYY-MM-DD) | 是 | 业务日期(入库到 `mall_daily_push.date`) |
+| `user_id` | string | 是 | PlayX 用户 ID(用于幂等;入库 `mall_daily_push.user_id` 等;服务端会按 `user_id`/`username` **确保存在** `mall_user_asset` 资产行) |
| `username` | string | 否 | 展示冗余(同步到商城用户侧逻辑时使用) |
| `yesterday_win_loss_net` | number | 否 | 昨日净输赢(仅当 `< 0` 时计算新增保障金) |
| `yesterday_total_deposit` | number | 否 | 昨日总充值(用于计算今日可领取上限) |
@@ -178,186 +181,40 @@ curl -X POST 'http://localhost:1818/api/v1/mall/dailyPush' \
---
-## 2. PlayX -> 积分商城(商城调用 PlayX)
+## 2. 积分商城 → PlayX(贵方需提供的 HTTP 接口)
-> 下面这些接口由 PlayX 提供。商城侧仅按“请求参数 + 期望返回判定条件”发起调用与处理结果。
-> **说明**:H5 调商城的 **`/api/v1/mall/verifyToken`** 在配置 **`playx.verify_token_local_only=true`**(默认)时**不会请求**本节接口,而是在商城内校验 `muser` token;远程对接 PlayX 时见 **3.3** 与下文 **2.1**。
+商城在验 Token、红利发放、交易轮询、Angpow 导入等场景会 **主动请求 PlayX / Cash Market**。
+**完整 URL、请求/响应字段、成功判定、与 Angpush 双路径关系、联调待办** 已单独整理,便于 **直接转发给 PlayX 平台**:
-### 2.1 Token Verification API(PlayX 侧实现,远程验证时使用)
-* 方法:`POST`
-* URL:`${playx.api.base_url}${playx.api.token_verify_url}`
- * 默认:`/api/v1/auth/verify-token`
+- **`docs/PlayX-接口待完善清单.md`**
-#### 请求 Body(商城侧发送)
-| 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
-| `request_id` | string | 是 | 形如 `mall_{uniqid}` |
-| `token` | string | 是 | 前端传入的 PlayX token |
-
-#### 返回(期望)
-商城侧校验:
-* HTTP 状态码必须为 `200`
-* 且响应体中必须包含 `user_id`
-
-期望字段(示例):
-| 字段 | 类型 | 说明 |
-|------|------|------|
-| `user_id` | string | 必选 |
-| `username` | string | 可选 |
-| `token_expire_at` | string | 可选(能被 `strtotime` 解析) |
-
-示例(成功):
-```json
-{
- "user_id": "U123",
- "username": "demo_user",
- "token_expire_at": "2026-04-01T12:00:00Z"
-}
-```
-
-示例(失败):
-```json
-{
- "message": "invalid token"
-}
-```
+本文 **§1** 仅描述「谁调用商城」;**§3** 描述「H5 调用商城」。
---
-### 2.2 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 | 是 | `MallPlayxOrder.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` |
-
-#### 返回(期望)
-商城侧判定:
-* HTTP 状态码 `200`
-* 且 `data.status === "accepted"`
-
-成功时读取:
-* `data.playx_transaction_id`
-
-失败时读取:
-* `data.message` 写入订单 `fail_reason`
-
-示例(accepted):
-```json
-{
- "status": "accepted",
- "playx_transaction_id": "PX_TX_001"
-}
-```
-
-示例(rejected):
-```json
-{
- "status": "rejected",
- "message": "insufficient balance"
-}
-```
-
----
-
-### 2.3 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 | 是 | `MallPlayxOrder.external_transaction_id` |
-| `user_id` | string | 是 | PlayX 用户 ID |
-| `amount` | number | 是 | `MallPlayxOrder.amount` |
-| `multiplier` | int | 是 | `MallPlayxOrder.multiplier` |
-
-#### 返回(期望)
-与 Bonus Grant 一致:
-* `data.status === "accepted"` -> 读取 `playx_transaction_id`
-* 否则 -> 读取 `message` 写入 `fail_reason`
-
-示例(accepted):
-```json
-{
- "status": "accepted",
- "playx_transaction_id": "PX_TX_002"
-}
-```
-
-示例(rejected):
-```json
-{
- "status": "rejected",
- "message": "insufficient balance"
-}
-```
-
----
-
-### 2.4 Transaction Status Query API(交易终态查询)
-* 方法:`GET`
-* URL:`${playx.api.base_url}${playx.api.transaction_status_url}`
- * 默认:`/api/v1/transaction/status`
-
-#### Query 参数
-| 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
-| `externalTransactionId` | string | 是 | 订单幂等键 `external_transaction_id` |
-
-#### 返回(期望)
-定时任务读取 `data.status`:
-* `COMPLETED`:商城将订单 `status` 更新为 `COMPLETED`
-* `FAILED` 或 `REJECTED`:商城将订单 `status=REJECTED`、`grant_status=FAILED_FINAL`,并退回积分
-* 失败信息取 `data.message` 写入订单 `fail_reason`
-
-示例(completed):
-```json
-{ "status": "COMPLETED" }
-```
-
-示例(failed):
-```json
-{ "status": "FAILED", "message": "grant rejected by PlayX" }
-```
-
----
-
-## 3. 积分商城 -> H5(服务端提供给 H5 的接口)
+## 3. 积分商城 → H5(服务端提供给 H5 的接口)
### 3.0 数据模型说明(与代码一致)
-* **商城用户**:表 `mall_user`(主键 `id`)。
-* **PlayX 资产扩展**:表 `mall_playx_user_asset`,与 `mall_user` **一对一**(`mall_user_id` 唯一,`playx_user_id` 唯一)。
-* **对外业务 ID**:接口里返回或订单里使用的 `user_id` 字符串多为 **PlayX 侧用户 ID**(`playx_user_id`);H5 临时登录场景若尚无真实 PlayX ID,会生成形如 **`mall_{mall_user.id}`** 的占位 ID(见 `temLogin`)。
-* **服务端内部**:`Playx` 控制器内部用 **`mall_user.id`**(整型)解析资产;`session_id` / `token` / `user_id` 会映射到该 `mall_user`。
+* **积分商城用户资产主表**:`mall_user_asset`(账号、积分、`playx_user_id` 等;H5 临时登录 `temLogin` 直接创建/复用该表行,**不依赖**独立会员 `user` 表)。
+* **会话缓存**:`mall_session`(字段含 `session_id`、`user_id`(此处存 **PlayX 侧用户标识字符串**,与 `mall_user_asset.playx_user_id` 一致)、`expire_time` 等)。
+* **统一订单**:`mall_order`(红利/实物/提现订单;`user_id` 字段为 **`playx_user_id` 字符串**)。
+* **对外业务 ID**:订单与推送中的 `user_id` 多为 **PlayX 用户 ID**(`playx_user_id`)。临时登录场景下,资产表会生成占位 ID,形如 **`mall_{mall_user_asset.id}`**(见 `MallUserAsset::ensureForUsername`)。
-### 3.1 鉴权解析规则(`resolveMallUserIdFromRequest`)
+### 3.1 鉴权解析规则(`resolvePlayxAssetIdFromRequest`)
-以下接口在服务端最终都会解析出 **商城用户 `mall_user.id`**,再按该用户查询 `mall_playx_user_asset` 等。
+以下接口在服务端最终都会解析出 **`mall_user_asset.id`(整型,资产表主键)**,再按该 ID 加载资产与关联数据。
优先级(由高到低):
1. **`session_id`**(`post` 优先,`get` 兼容)
- * 在 `mall_playx_session` 中存在且未过期:用会话里的 `user_id`(即 `playx_user_id`)在 `mall_playx_user_asset` 反查 `mall_user_id`。
+ * 在 `mall_session` 中存在且未过期:用会话里的 `user_id`(`playx_user_id` 字符串)在 `mall_user_asset` 按 `playx_user_id` 查找,得到资产主键。
* 若会话无效:兼容把 `session_id` 参数误当作 **商城 token** 再试一次(UUID 形态 token)。
2. **`token`**(`post` / `get` 或请求头 **`ba-token`** / **`token`**)
- * 校验 `token` 表:类型为会员 `user` 或商城临时 **`muser`**(`mall_user` 登录),未过期则 `user_id` 字段即为 **`mall_user.id`**。
+ * 校验 `token` 表:类型为会员 `user` 或商城临时 **`muser`**。未过期时,`user_id` 字段为 **`mall_user_asset.id`**(`muser`)或会员体系约定 ID(`user`)。
3. **`user_id`**(`post` / `get` 兼容)
- * **纯数字**:视为 **`mall_user.id`**。
- * **非纯数字**:视为 **`playx_user_id`**,在 `mall_playx_user_asset` 查找对应 `mall_user_id`。
+ * **纯数字**:视为 **`mall_user_asset.id`**。
+ * **非纯数字**:视为 **`playx_user_id`**,在 `mall_user_asset` 按该字段查找主键。
> 注意:请求参数的取值方式是 `post()` 优先,`get()` 兼容(即同字段既可传 post 也可传 get)。
@@ -373,19 +230,18 @@ curl -X POST 'http://localhost:1818/api/v1/mall/dailyPush' \
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
-| `username` | string | 是 | 商城用户名(唯一);不存在则自动创建 `mall_user` |
+| `username` | string | 是 | 登录名(唯一);不存在则自动创建 `mall_user_asset` 行 |
#### 行为说明
-* 若 `mall_user` 不存在:创建用户(随机占位手机号、随机密码等,与后台「商城用户」一致)。
-* **无论是否新用户**:保证存在 **`mall_playx_user_asset`** 一条记录(`MallPlayxUserAsset::ensureForMallUser`),`playx_user_id` 默认 **`mall_{mall_user.id}`**(与 PlayX 真实 ID 冲突概率低)。
-* 签发 **商城 token**(类型 **`muser`**,非会员表 `user`),并签发 `muser-refresh` 刷新令牌。
+* 若用户名不存在:`MallUserAsset::ensureForUsername` 创建资产行(随机密码等),并将 `playx_user_id` 更新为 **`mall_{id}`** 形式(与真实 PlayX ID 区分)。
+* 签发 **商城 token**(类型 **`muser`**,`token` 表内 `user_id` = **`mall_user_asset.id`**),并签发 `muser-refresh` 刷新令牌。
#### 返回(成功 data.userInfo)
| 字段 | 类型 | 说明 |
|------|------|------|
-| `id` | int | `mall_user.id` |
+| `id` | int | **`mall_user_asset.id`** |
| `username` | string | 用户名 |
| `nickname` | string | 同 `username` |
| `playx_user_id` | string | 资产表中的 `playx_user_id`(如 `mall_12`) |
@@ -413,10 +269,10 @@ curl -G 'http://localhost:1818/api/v1/temLogin' --data-urlencode 'username=demo_
* 配置项:`config/playx.php` → **`verify_token_local_only`**(环境变量 **`PLAYX_VERIFY_TOKEN_LOCAL_ONLY`**,未设置时默认为 **`1` / 开启本地验证)。
* **`verify_token_local_only = true`(默认)**
* **不请求** PlayX HTTP。
- * 仅接受商城临时登录 token(类型 **`muser`**),校验 `token` 表后,根据 `mall_user` 与 `mall_playx_user_asset` 写入 `mall_playx_session`。
- * 返回的 `data.user_id` 为 **`playx_user_id`**(无资产记录时回退为 `mall_user.id` 字符串,一般 temLogin 后已有资产)。
+ * 仅接受商城临时登录 token(类型 **`muser`**),校验 `token` 表后写入 **`mall_session`**。
+ * 返回的 `data.user_id` 为 **`playx_user_id`**(与资产表一致)。
* **`verify_token_local_only = false`**(生产对接 PlayX)
- * 需配置 **`playx.api.base_url`**,由商城向 PlayX 发起 `POST` 校验(见下文「远程模式」)。
+ * 需配置 **`playx.api.base_url`**,由商城向 PlayX 发起 `POST` 校验(请求/响应约定见 **`docs/PlayX-接口待完善清单.md`** 第一部分 §1)。
* 若未配置 `base_url`,返回 `PlayX API not configured`。
#### 请求参数
@@ -429,7 +285,7 @@ curl -G 'http://localhost:1818/api/v1/temLogin' --data-urlencode 'username=demo_
| 字段 | 类型 | 说明 |
|------|------|------|
-| `session_id` | string | 写入 `mall_playx_session` |
+| `session_id` | string | 写入 `mall_session` |
| `user_id` | string | PlayX 用户 ID(即 `playx_user_id`,会话内与订单/推送一致) |
| `username` | string | 用户名 |
| `token_expire_at` | string | ISO 字符串(服务端 `date('c', expireAt)`) |
@@ -484,37 +340,6 @@ Body:`id` 必填,其余字段按需传入更新。
Body:`id` 必填。若删除默认地址,服务端会自动挑选一条剩余地址设为默认(如存在)。
-#### 远程模式(`verify_token_local_only=false` + 已配置 `base_url`)
-
-商城侧请求 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`
-
-响应(成功示例):
-
-```json
-{
- "code": 1,
- "msg": "",
- "data": {
- "session_id": "7b1c....",
- "user_id": "U123",
- "username": "demo_user",
- "token_expire_at": "2026-04-01T12:00:00+00:00"
- }
-}
-```
-
---
### 3.4 用户资产(Assets)
@@ -527,7 +352,7 @@ Body:`id` 必填。若删除默认地址,服务端会自动挑选一条剩
* `session_id`
* `token`(或请求头 `ba-token` / `token`)
-* `user_id`(纯数字为 `mall_user.id`,否则为 `playx_user_id`)
+* `user_id`(纯数字为 **`mall_user_asset.id`**,否则为 **`playx_user_id`**)
#### 返回(成功 data)
若未找到资产:返回 0。
@@ -784,7 +609,7 @@ curl -X POST 'http://localhost:1818/api/v1/mall/withdrawApply' \
#### 返回(成功 data)
* `list`:订单列表(最多 100 条),并包含关联的 `mallItem`(关系对象)
-* 列表项中的 `user_id` 为 **PlayX 侧 `playx_user_id`**(字符串),与 `mall_playx_order.user_id` 一致
+* 列表项中的 `user_id` 为 **PlayX 侧 `playx_user_id`**(字符串),与 `mall_order.user_id` 一致
#### 示例
请求:
diff --git a/docs/积分商城-PlayX对接实施方案.md b/docs/积分商城-PlayX对接实施方案.md
index be42de9..3315622 100644
--- a/docs/积分商城-PlayX对接实施方案.md
+++ b/docs/积分商城-PlayX对接实施方案.md
@@ -526,7 +526,7 @@ curl -G 'http://localhost:1818/api/v1/mall/orders' --data-urlencode 'session_id=
- 发货:录入物流公司、单号 → `SHIPPED`
- 驳回:录入驳回原因 → `REJECTED`,自动退回积分
- **红利/提现订单**:
- - 展示 `external_transaction_id`、`playx_transaction_id`、推送playx
+ - 展示 `external_transaction_id`、推送 playx 状态
- 手动重试:仅对 `FAILED_RETRYABLE` 状态,记录 `retry_request_id`、操作者、原因
### 2.3 用户资产与人工调账
@@ -613,8 +613,7 @@ curl -G 'http://localhost:1818/api/v1/mall/orders' --data-urlencode 'session_id=
| points_cost | int | 消耗积分 |
| amount | decimal(15,2) | 现金面值(红利/提现) |
| multiplier | int | 流水倍数 |
-| external_transaction_id | varchar(64) | 订单号 |
-| playx_transaction_id | varchar(64) | PlayX 流水号 |
+| external_transaction_id | varchar(64) | 订单号(商城侧幂等/对账主键;Angpow 流程不单独落库 PlayX 内部流水号) |
| grant_status | enum | NOT_SENT / SENT_PENDING / ACCEPTED / FAILED_RETRYABLE / FAILED_FINAL |
| fail_reason | text | 失败原因 |
| retry_count | int | 重试次数 |
diff --git a/web/src/components/table/fieldRender/failReason.vue b/web/src/components/table/fieldRender/failReason.vue
new file mode 100644
index 0000000..7f10cba
--- /dev/null
+++ b/web/src/components/table/fieldRender/failReason.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
+ {{ fullText }}
+
+ {{ displayText }}
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/lang/backend/en/mall/order.ts b/web/src/lang/backend/en/mall/order.ts
index eca0884..0062ce0 100644
--- a/web/src/lang/backend/en/mall/order.ts
+++ b/web/src/lang/backend/en/mall/order.ts
@@ -19,7 +19,6 @@ export default {
amount: 'Cash amount',
multiplier: 'Turnover multiplier',
external_transaction_id: 'Order number',
- playx_transaction_id: 'PlayX transaction ID',
grant_status: 'Grant status',
'grant_status NOT_SENT': 'Not sent',
'grant_status SENT_PENDING': 'Sent (queued)',
diff --git a/web/src/lang/backend/en/mall/playxOrder.ts b/web/src/lang/backend/en/mall/playxOrder.ts
index 9ede71e..1b94ccb 100644
--- a/web/src/lang/backend/en/mall/playxOrder.ts
+++ b/web/src/lang/backend/en/mall/playxOrder.ts
@@ -16,7 +16,6 @@ export default {
amount: 'amount',
multiplier: 'multiplier',
external_transaction_id: 'external_transaction_id',
- playx_transaction_id: 'playx_transaction_id',
grant_status: 'grant_status',
'grant_status NOT_SENT': 'NOT_SENT',
'grant_status SENT_PENDING': 'SENT_PENDING',
diff --git a/web/src/lang/backend/zh-cn/mall/claimLog.ts b/web/src/lang/backend/zh-cn/mall/claimLog.ts
index ce7981f..2e7bcc4 100644
--- a/web/src/lang/backend/zh-cn/mall/claimLog.ts
+++ b/web/src/lang/backend/zh-cn/mall/claimLog.ts
@@ -1,7 +1,7 @@
export default {
id: 'ID',
claim_request_id: '领取订单号',
- user_id: '用户ID',
+ user_id: 'Playx-ID',
claimed_amount: '领取积分',
create_time: '创建时间',
'quick Search Fields': 'ID',
diff --git a/web/src/lang/backend/zh-cn/mall/dailyPush.ts b/web/src/lang/backend/zh-cn/mall/dailyPush.ts
index cdf1f0e..953de3a 100644
--- a/web/src/lang/backend/zh-cn/mall/dailyPush.ts
+++ b/web/src/lang/backend/zh-cn/mall/dailyPush.ts
@@ -1,6 +1,6 @@
export default {
id: 'ID',
- user_id: '用户ID',
+ user_id: 'Playx-ID',
date: '业务日期',
username: '用户名',
yesterday_win_loss_net: '昨日净输赢',
diff --git a/web/src/lang/backend/zh-cn/mall/order.ts b/web/src/lang/backend/zh-cn/mall/order.ts
index 24de560..ca72000 100644
--- a/web/src/lang/backend/zh-cn/mall/order.ts
+++ b/web/src/lang/backend/zh-cn/mall/order.ts
@@ -3,23 +3,22 @@ export default {
manual_retry: '手动重试',
retry_confirm: '确认将该订单加入重试队列?',
id: 'ID',
- user_id: '用户ID',
+ user_id: 'Playx-ID',
type: '类型',
'type BONUS': '红利(BONUS)',
'type PHYSICAL': '实物(PHYSICAL)',
'type WITHDRAW': '提现(WITHDRAW)',
status: '状态',
- 'status PENDING': '处理中(PENDING)',
- 'status COMPLETED': '已完成(COMPLETED)',
- 'status SHIPPED': '已发货(SHIPPED)',
- 'status REJECTED': '已驳回(REJECTED)',
+ 'status PENDING': '处理中',
+ 'status COMPLETED': '已完成',
+ 'status SHIPPED': '已发货',
+ 'status REJECTED': '已驳回',
mall_item_id: '商品ID',
mallitem__title: '商品标题',
points_cost: '消耗积分',
amount: '现金面值',
multiplier: '流水倍数',
external_transaction_id: '订单号',
- playx_transaction_id: 'PlayX流水号',
grant_status: '推送playx状态',
'grant_status NOT_SENT': '未发送',
'grant_status SENT_PENDING': '已发送排队',
diff --git a/web/src/lang/backend/zh-cn/mall/playxClaimLog.ts b/web/src/lang/backend/zh-cn/mall/playxClaimLog.ts
index ce7981f..2e7bcc4 100644
--- a/web/src/lang/backend/zh-cn/mall/playxClaimLog.ts
+++ b/web/src/lang/backend/zh-cn/mall/playxClaimLog.ts
@@ -1,7 +1,7 @@
export default {
id: 'ID',
claim_request_id: '领取订单号',
- user_id: '用户ID',
+ user_id: 'Playx-ID',
claimed_amount: '领取积分',
create_time: '创建时间',
'quick Search Fields': 'ID',
diff --git a/web/src/lang/backend/zh-cn/mall/playxDailyPush.ts b/web/src/lang/backend/zh-cn/mall/playxDailyPush.ts
index cdf1f0e..953de3a 100644
--- a/web/src/lang/backend/zh-cn/mall/playxDailyPush.ts
+++ b/web/src/lang/backend/zh-cn/mall/playxDailyPush.ts
@@ -1,6 +1,6 @@
export default {
id: 'ID',
- user_id: '用户ID',
+ user_id: 'Playx-ID',
date: '业务日期',
username: '用户名',
yesterday_win_loss_net: '昨日净输赢',
diff --git a/web/src/lang/backend/zh-cn/mall/playxOrder.ts b/web/src/lang/backend/zh-cn/mall/playxOrder.ts
index 187f240..e9b78ae 100644
--- a/web/src/lang/backend/zh-cn/mall/playxOrder.ts
+++ b/web/src/lang/backend/zh-cn/mall/playxOrder.ts
@@ -1,6 +1,6 @@
export default {
id: 'ID',
- user_id: '用户ID',
+ user_id: 'Playx-ID',
type: '类型',
'type BONUS': '红利(BONUS)',
'type PHYSICAL': '实物(PHYSICAL)',
@@ -16,7 +16,6 @@ export default {
amount: '现金面值',
multiplier: '流水倍数',
external_transaction_id: '订单号',
- playx_transaction_id: 'PlayX流水号',
grant_status: '推送playx状态',
'grant_status NOT_SENT': '未发送',
'grant_status SENT_PENDING': '已发送排队',
diff --git a/web/src/lang/backend/zh-cn/mall/playxUserAsset.ts b/web/src/lang/backend/zh-cn/mall/playxUserAsset.ts
index 3e8aaaa..b99bb3d 100644
--- a/web/src/lang/backend/zh-cn/mall/playxUserAsset.ts
+++ b/web/src/lang/backend/zh-cn/mall/playxUserAsset.ts
@@ -2,7 +2,7 @@ export default {
id: 'ID',
username: '用户名',
phone: '手机号',
- playx_user_id: 'PlayX用户ID',
+ playx_user_id: 'PlayX-ID',
locked_points: '待领取积分',
available_points: '可用积分',
today_limit: '今日可领取上限',
diff --git a/web/src/lang/backend/zh-cn/mall/userAsset.ts b/web/src/lang/backend/zh-cn/mall/userAsset.ts
index 7aca0db..e8b91d7 100644
--- a/web/src/lang/backend/zh-cn/mall/userAsset.ts
+++ b/web/src/lang/backend/zh-cn/mall/userAsset.ts
@@ -2,7 +2,7 @@ export default {
id: 'ID',
username: '用户名',
phone: '手机号',
- playx_user_id: 'PlayX用户ID',
+ playx_user_id: 'PlayX-ID',
locked_points: '待领取积分',
available_points: '可用积分',
today_limit: '今日可领取上限',
diff --git a/web/src/lang/backend/zh-cn/user/moneyLog.ts b/web/src/lang/backend/zh-cn/user/moneyLog.ts
index 7f40880..68853e8 100644
--- a/web/src/lang/backend/zh-cn/user/moneyLog.ts
+++ b/web/src/lang/backend/zh-cn/user/moneyLog.ts
@@ -2,7 +2,7 @@ export default {
'User name': '用户名',
'User nickname': '用户昵称',
balance: '余额',
- 'User ID': '用户ID',
+ 'User ID': 'Playx-ID',
'Change balance': '变更余额',
'Before change': '变更前',
'After change': '变更后',
diff --git a/web/src/utils/axios.ts b/web/src/utils/axios.ts
index 52bebaf..bd3cecc 100644
--- a/web/src/utils/axios.ts
+++ b/web/src/utils/axios.ts
@@ -12,6 +12,26 @@ import { SYSTEM_ZINDEX } from '/@/stores/constant/common'
import { useUserInfo } from '/@/stores/userInfo'
import { isAdminApp } from '/@/utils/common'
+/** 与后台 LoadLangPack 一致:优先与当前 i18n 语言对齐,再回落到 config */
+function resolveAdminThinkLang(): string {
+ const cfg = useConfig()
+ try {
+ const loc = i18n?.global?.locale?.value
+ if (loc !== undefined && loc !== null && loc !== '') {
+ const v = String(loc).toLowerCase().replace('_', '-')
+ if (v === 'zh-cn' || v === 'zh') {
+ return 'zh-cn'
+ }
+ if (v === 'en') {
+ return 'en'
+ }
+ }
+ } catch {
+ // i18n 未就绪时忽略
+ }
+ return cfg.lang.defaultLang
+}
+
window.requests = []
window.tokenRefreshing = false
const pendingMap = new Map()
@@ -50,7 +70,7 @@ function createAxios>(axiosConfig: AxiosRequest
baseURL: getUrl(),
timeout: 1000 * 10,
headers: {
- 'think-lang': config.lang.defaultLang,
+ 'think-lang': resolveAdminThinkLang(),
},
responseType: 'json',
})
@@ -93,7 +113,7 @@ function createAxios>(axiosConfig: AxiosRequest
if (token) (config.headers as anyObj).batoken = token
const userToken = options.anotherToken || userInfo.getToken()
if (userToken) (config.headers as anyObj)['ba-user-token'] = userToken
- ;(config.headers as anyObj)['think-lang'] = useConfig().lang.defaultLang
+ ;(config.headers as anyObj)['think-lang'] = resolveAdminThinkLang()
}
return config
diff --git a/web/src/views/backend/mall/order/index.vue b/web/src/views/backend/mall/order/index.vue
index 6efb7d2..0cb97b6 100644
--- a/web/src/views/backend/mall/order/index.vue
+++ b/web/src/views/backend/mall/order/index.vue
@@ -30,6 +30,7 @@ defineOptions({
const { t } = useI18n()
const tableRef = useTemplateRef('tableRef')
+
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete']).map((btn) =>
btn.name === 'edit'
? {
@@ -80,7 +81,7 @@ const baTable = new baTableClass(
align: 'center',
effect: 'dark',
custom: { PENDING: 'success', COMPLETED: 'primary', SHIPPED: 'info', REJECTED: 'loading' },
- minWidth: 160,
+ minWidth: 100,
operator: 'eq',
sortable: false,
render: 'tag',
@@ -91,7 +92,14 @@ const baTable = new baTableClass(
REJECTED: t('mall.order.status REJECTED'),
},
},
- { label: t('mall.order.mall_item_id'), prop: 'mall_item_id', align: 'center', operator: 'RANGE', sortable: false },
+ {
+ label: t('mall.order.mall_item_id'),
+ prop: 'mall_item_id',
+ align: 'center',
+ show: false,
+ operator: 'RANGE',
+ sortable: false,
+ },
{
label: t('mall.order.mallitem__title'),
prop: 'mallItem.title',
@@ -114,14 +122,6 @@ const baTable = new baTableClass(
sortable: false,
operator: 'LIKE',
},
- {
- label: t('mall.order.playx_transaction_id'),
- prop: 'playx_transaction_id',
- align: 'center',
- operatorPlaceholder: t('Fuzzy query'),
- sortable: false,
- operator: 'LIKE',
- },
{
label: t('mall.order.grant_status'),
prop: 'grant_status',
@@ -151,7 +151,8 @@ const baTable = new baTableClass(
label: t('mall.order.fail_reason'),
prop: 'fail_reason',
align: 'center',
- showOverflowTooltip: true,
+ minWidth: 140,
+ render: 'failReason',
operatorPlaceholder: t('Fuzzy query'),
sortable: false,
operator: 'LIKE',
@@ -160,7 +161,8 @@ const baTable = new baTableClass(
label: t('mall.order.reject_reason'),
prop: 'reject_reason',
align: 'center',
- showOverflowTooltip: true,
+ minWidth: 140,
+ render: 'failReason',
sortable: false,
operator: 'LIKE',
operatorPlaceholder: t('Fuzzy query'),
@@ -315,5 +317,3 @@ onMounted(() => {
})
})
-
-
diff --git a/web/src/views/backend/mall/playxOrder/index.vue b/web/src/views/backend/mall/playxOrder/index.vue
index 2512cde..2992ca8 100644
--- a/web/src/views/backend/mall/playxOrder/index.vue
+++ b/web/src/views/backend/mall/playxOrder/index.vue
@@ -69,7 +69,6 @@ const baTable = new baTableClass(
{ label: t('mall.playxOrder.amount'), prop: 'amount', align: 'center', operator: 'RANGE', sortable: false },
{ label: t('mall.playxOrder.multiplier'), prop: 'multiplier', align: 'center', operator: 'eq', sortable: false },
{ label: t('mall.playxOrder.external_transaction_id'), prop: 'external_transaction_id', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
- { label: t('mall.playxOrder.playx_transaction_id'), prop: 'playx_transaction_id', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' },
{
label: t('mall.playxOrder.grant_status'),
prop: 'grant_status',
diff --git a/web/types/tableRenderer.d.ts b/web/types/tableRenderer.d.ts
index 8b5a7db..f59846d 100644
--- a/web/types/tableRenderer.d.ts
+++ b/web/types/tableRenderer.d.ts
@@ -6,6 +6,7 @@ type TableRenderer =
| 'customTemplate'
| 'date'
| 'datetime'
+ | 'failReason'
| 'icon'
| 'image'
| 'images'