1.优化后台页面样式

2.优化统一订单中红利的状态和失败原因
3.移除项目中冗余代码和字段
This commit is contained in:
2026-04-21 11:59:15 +08:00
parent 1c900e7132
commit 3ac825f15d
26 changed files with 199 additions and 264 deletions

View File

@@ -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();

View File

@@ -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',
];

View File

@@ -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' => '驳回成功',
];

View File

@@ -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 {

View File

@@ -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' => '',
];
}
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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. 积分商城 -> PlayXPlayX 调用商城
## 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 APIPlayX 侧实现,远程验证时使用)
* 方法:`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` 一致
#### 示例
请求:

View File

@@ -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 | 重试次数 |

View File

@@ -0,0 +1,75 @@
<template>
<div class="mall-order-fail-reason-table-cell">
<template v-if="fullText !== ''">
<el-tooltip placement="top" effect="dark" popper-class="mall-order-reason-tooltip" :enterable="true">
<template #content>
<div class="mall-order-fail-reason-tooltip-body">{{ fullText }}</div>
</template>
<span class="mall-order-fail-reason-ellipsis">{{ displayText }}</span>
</el-tooltip>
</template>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { TableColumnCtx } from 'element-plus'
import { getCellValue } from '/@/components/table/index'
interface Props {
row: TableRow
field: TableColumn
column: TableColumnCtx<TableRow>
index: number
}
const props = defineProps<Props>()
/** 与后台换行一致;无换行时在 attempt N: 前补行首,便于阅读 */
function normalizeFailReasonText(raw: unknown): string {
if (raw === null || raw === undefined) {
return ''
}
let t = String(raw)
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n')
t = t.replace(/([^\n])(attempt\s+\d+:)/gi, '$1\n$2')
return t
}
const rawCell = computed(() => getCellValue(props.row, props.field, props.column, props.index))
const fullText = computed(() => normalizeFailReasonText(rawCell.value))
const displayText = computed(() => fullText.value)
</script>
<style scoped lang="scss">
.mall-order-fail-reason-table-cell {
width: 100%;
min-width: 0;
}
.mall-order-fail-reason-ellipsis {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: bottom;
}
</style>
<style lang="scss">
.el-popper.mall-order-reason-tooltip {
max-width: min(560px, 90vw);
box-sizing: border-box;
}
.mall-order-reason-tooltip .mall-order-fail-reason-tooltip-body {
white-space: pre-wrap;
word-break: break-word;
line-height: 1.5;
text-align: left;
}
</style>

View File

@@ -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)',

View File

@@ -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',

View File

@@ -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',

View File

@@ -1,6 +1,6 @@
export default {
id: 'ID',
user_id: '用户ID',
user_id: 'Playx-ID',
date: '业务日期',
username: '用户名',
yesterday_win_loss_net: '昨日净输赢',

View File

@@ -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': '已发送排队',

View File

@@ -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',

View File

@@ -1,6 +1,6 @@
export default {
id: 'ID',
user_id: '用户ID',
user_id: 'Playx-ID',
date: '业务日期',
username: '用户名',
yesterday_win_loss_net: '昨日净输赢',

View File

@@ -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': '已发送排队',

View File

@@ -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: '今日可领取上限',

View File

@@ -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: '今日可领取上限',

View File

@@ -2,7 +2,7 @@ export default {
'User name': '用户名',
'User nickname': '用户昵称',
balance: '余额',
'User ID': '用户ID',
'User ID': 'Playx-ID',
'Change balance': '变更余额',
'Before change': '变更前',
'After change': '变更后',

View File

@@ -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<Data = any, T = ApiPromise<Data>>(axiosConfig: AxiosRequest
baseURL: getUrl(),
timeout: 1000 * 10,
headers: {
'think-lang': config.lang.defaultLang,
'think-lang': resolveAdminThinkLang(),
},
responseType: 'json',
})
@@ -93,7 +113,7 @@ function createAxios<Data = any, T = ApiPromise<Data>>(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

View File

@@ -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(() => {
})
})
</script>
<style scoped lang="scss"></style>

View File

@@ -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',

View File

@@ -6,6 +6,7 @@ type TableRenderer =
| 'customTemplate'
| 'date'
| 'datetime'
| 'failReason'
| 'icon'
| 'image'
| 'images'