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