From 520e950dc500240e592a7b75c5b4b1d1b36468c8 Mon Sep 17 00:00:00 2001 From: zhenhui <1276357500@qq.com> Date: Tue, 31 Mar 2026 15:37:32 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8E=A5=E5=8F=A3=E4=BB=A5?= =?UTF-8?q?=E5=8F=8A=E5=90=8E=E5=8F=B0=E9=A1=B5=E9=9D=A2=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/controller/mall/Order.php | 52 +++- app/api/controller/v1/Auth.php | 18 ++ app/api/controller/v1/Playx.php | 76 +++-- app/api/lang/en.php | 2 + app/api/lang/zh-cn.php | 2 + app/common/model/MallAddress.php | 36 ++- app/common/model/MallOrder.php | 9 +- app/common/model/MallUserAsset.php | 27 +- config/route.php | 26 +- docs/H5-积分商城接口文档.md | 205 +++++++++++++ docs/PlayX-接口文档.md | 62 ++-- docs/PlayX-调用积分商城接口说明.md | 66 ++-- docs/积分商城-PlayX对接实施方案.md | 93 +++--- web/src/lang/backend/en/mall/address.ts | 2 +- web/src/lang/backend/en/mall/order.ts | 1 + web/src/lang/backend/zh-cn/mall/address.ts | 2 +- web/src/lang/backend/zh-cn/mall/claimLog.ts | 2 +- web/src/lang/backend/zh-cn/mall/order.ts | 3 +- .../lang/backend/zh-cn/mall/playxClaimLog.ts | 2 +- web/src/lang/backend/zh-cn/mall/playxOrder.ts | 2 +- web/src/views/backend/mall/address/index.vue | 36 ++- .../views/backend/mall/address/popupForm.vue | 17 +- .../views/backend/mall/dailyPush/index.vue | 76 ++++- web/src/views/backend/mall/item/index.vue | 5 +- web/src/views/backend/mall/order/index.vue | 244 +++++++++------ .../views/backend/mall/order/popupForm.vue | 290 ++++++++++++++++++ .../views/backend/mall/userAsset/index.vue | 72 ++++- .../backend/mall/userAsset/popupForm.vue | 124 ++++++++ 28 files changed, 1241 insertions(+), 311 deletions(-) create mode 100644 docs/H5-积分商城接口文档.md create mode 100644 web/src/views/backend/mall/order/popupForm.vue create mode 100644 web/src/views/backend/mall/userAsset/popupForm.vue diff --git a/app/admin/controller/mall/Order.php b/app/admin/controller/mall/Order.php index 6b900a9..6d6cd6d 100644 --- a/app/admin/controller/mall/Order.php +++ b/app/admin/controller/mall/Order.php @@ -46,6 +46,7 @@ class Order extends Backend 'receiver_name', 'receiver_phone', 'receiver_address', + 'mall_address_id', 'create_time', 'update_time', ]; @@ -139,7 +140,52 @@ class Order extends Backend } /** - * PHYSICAL 驳回:更新状态为 REJECTED,并退回积分到 available_points + * 审核通过(非 PHYSICAL):更新状态为 COMPLETED + */ + public function approve(Request $request): Response + { + $response = $this->initializeBackend($request); + if ($response !== null) { + return $response; + } + + if ($request->method() !== 'POST') { + return $this->error(__('Parameter error')); + } + + $id = $request->post('id', 0); + if (!$id) { + return $this->error(__('Missing required fields')); + } + + $order = MallOrder::where('id', $id)->find(); + if (!$order) { + return $this->error(__('Record not found')); + } + if ($order->status !== MallOrder::STATUS_PENDING) { + return $this->error(__('Order status must be PENDING')); + } + if ($order->type === MallOrder::TYPE_PHYSICAL) { + return $this->error(__('Order type not supported')); + } + + Db::startTrans(); + try { + $order->status = MallOrder::STATUS_COMPLETED; + $order->update_time = time(); + $order->save(); + + Db::commit(); + } catch (Throwable $e) { + Db::rollback(); + return $this->error($e->getMessage()); + } + + return $this->success(__('Approved successfully')); + } + + /** + * 审核驳回:更新状态为 REJECTED,并退回积分到 available_points(所有类型通用) */ public function reject(Request $request): Response { @@ -164,9 +210,6 @@ class Order extends Backend if (!$order) { return $this->error(__('Record not found')); } - if ($order->type !== MallOrder::TYPE_PHYSICAL) { - return $this->error(__('Order type not PHYSICAL')); - } if ($order->status !== MallOrder::STATUS_PENDING) { return $this->error(__('Order status must be PENDING')); } @@ -187,6 +230,7 @@ class Order extends Backend $order->status = MallOrder::STATUS_REJECTED; $order->reject_reason = $rejectReason; $order->grant_status = MallOrder::GRANT_FAILED_FINAL; + $order->update_time = time(); $order->save(); Db::commit(); diff --git a/app/api/controller/v1/Auth.php b/app/api/controller/v1/Auth.php index 9c0df48..ed5a642 100644 --- a/app/api/controller/v1/Auth.php +++ b/app/api/controller/v1/Auth.php @@ -115,6 +115,24 @@ class Auth extends Api } $username = trim(strval($request->get('username', $request->post('username', '')))); + // 兼容:querystring 中未编码的 '+' 会被解析为空格(application/x-www-form-urlencoded 规则) + // 例如:/api/v1/temLogin?username=+607... 期望保留 '+',则从原始 querystring 提取并还原 + if ($username !== '' && str_contains($username, ' ')) { + $qs = $request->queryString(); + if (is_string($qs) && $qs !== '') { + foreach (explode('&', $qs) as $pair) { + if ($pair === '' || !str_contains($pair, '=')) { + continue; + } + [$k, $v] = explode('=', $pair, 2); + if (rawurldecode($k) === 'username') { + // 先把 %xx 解码;注意这里不把 '+' 当空格处理,从而保留 '+' + $username = trim(rawurldecode($v)); + break; + } + } + } + } if ($username === '') { return $this->error(__('Parameter username can not be empty')); } diff --git a/app/api/controller/v1/Playx.php b/app/api/controller/v1/Playx.php index a85b81e..12b076c 100644 --- a/app/api/controller/v1/Playx.php +++ b/app/api/controller/v1/Playx.php @@ -382,7 +382,7 @@ class Playx extends Api $token = strval($request->post('token', $request->post('session', $request->get('token', '')))); if ($token === '') { - return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]); + return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]); } if (config('playx.verify_token_local_only', false)) { @@ -410,7 +410,7 @@ class Playx extends Api $data = json_decode(strval($res->getBody()), true); if ($code !== 200 || empty($data['user_id'])) { $remoteMsg = $data['message'] ?? ''; - $msg = is_string($remoteMsg) && $remoteMsg !== '' ? $remoteMsg : __('Invalid token'); + $msg = is_string($remoteMsg) && $remoteMsg !== '' ? $remoteMsg : __('Token expiration'); return $this->error($msg, null, 0, ['statusCode' => 401]); } @@ -454,20 +454,20 @@ class Playx extends Api { $tokenData = Token::get($token); if (empty($tokenData) || (isset($tokenData['expire_time']) && intval($tokenData['expire_time']) <= time())) { - return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]); + return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]); } $tokenType = strval($tokenData['type'] ?? ''); if ($tokenType !== UserAuth::TOKEN_TYPE_MALL_USER) { - return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]); + return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]); } $assetId = intval($tokenData['user_id'] ?? 0); if ($assetId <= 0) { - return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]); + return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]); } $asset = MallUserAsset::where('id', $assetId)->find(); if (!$asset) { - return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]); + return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]); } $playxUserId = strval($asset->playx_user_id ?? ''); @@ -507,7 +507,7 @@ class Playx extends Api $assetId = $this->resolvePlayxAssetIdFromRequest($request); if ($assetId === null) { - return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]); + return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]); } $asset = $this->getAssetById($assetId); @@ -546,7 +546,10 @@ class Playx extends Api $claimRequestId = strval($request->post('claim_request_id', '')); $assetId = $this->resolvePlayxAssetIdFromRequest($request); - if ($claimRequestId === '' || $assetId === null) { + if ($assetId === null) { + return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]); + } + if ($claimRequestId === '') { return $this->error(__('claim_request_id and user_id/session_id required')); } @@ -660,7 +663,7 @@ class Playx extends Api $assetId = $this->resolvePlayxAssetIdFromRequest($request); if ($assetId === null) { - return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]); + return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]); } $asset = $this->getAssetById($assetId); if (!$asset || strval($asset->playx_user_id ?? '') === '') { @@ -689,7 +692,7 @@ class Playx extends Api $assetId = $this->resolvePlayxAssetIdFromRequest($request); if ($assetId === null) { - return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]); + return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]); } $list = MallAddress::where('playx_user_asset_id', $assetId) @@ -713,16 +716,16 @@ class Playx extends Api $assetId = $this->resolvePlayxAssetIdFromRequest($request); if ($assetId === null) { - return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]); + return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]); } $phone = trim(strval($request->post('phone', ''))); + $receiverName = trim(strval($request->post('receiver_name', ''))); $region = $request->post('region', ''); $detailAddress = trim(strval($request->post('detail_address', ''))); - $address = trim(strval($request->post('address', ''))); $defaultSetting = strval($request->post('default_setting', '0')) === '1' ? 1 : 0; - if ($phone === '' || $detailAddress === '' || $address === '' || $region === '' || $region === null) { + if ($phone === '' || $receiverName === '' || $detailAddress === '' || $region === '' || $region === null) { return $this->error(__('Missing required fields')); } @@ -734,10 +737,10 @@ class Playx extends Api $created = MallAddress::create([ 'playx_user_asset_id' => $assetId, + 'receiver_name' => $receiverName, 'phone' => $phone, 'region' => $region, 'detail_address' => $detailAddress, - 'address' => $address, 'default_setting' => $defaultSetting, 'create_time' => time(), 'update_time' => time(), @@ -767,7 +770,7 @@ class Playx extends Api $assetId = $this->resolvePlayxAssetIdFromRequest($request); if ($assetId === null) { - return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]); + return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]); } $id = intval($request->post('id', 0)); @@ -784,15 +787,15 @@ class Playx extends Api if ($request->post('phone', null) !== null) { $updates['phone'] = trim(strval($request->post('phone', ''))); } + if ($request->post('receiver_name', null) !== null) { + $updates['receiver_name'] = trim(strval($request->post('receiver_name', ''))); + } if ($request->post('region', null) !== null) { $updates['region'] = $request->post('region', ''); } if ($request->post('detail_address', null) !== null) { $updates['detail_address'] = trim(strval($request->post('detail_address', ''))); } - if ($request->post('address', null) !== null) { - $updates['address'] = trim(strval($request->post('address', ''))); - } if ($request->post('default_setting', null) !== null) { $updates['default_setting'] = strval($request->post('default_setting', '0')) === '1' ? 1 : 0; } @@ -830,7 +833,7 @@ class Playx extends Api $assetId = $this->resolvePlayxAssetIdFromRequest($request); if ($assetId === null) { - return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]); + return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]); } $id = intval($request->post('id', 0)); @@ -897,7 +900,10 @@ class Playx extends Api $itemId = intval($request->post('item_id', 0)); $assetId = $this->resolvePlayxAssetIdFromRequest($request); - if ($itemId <= 0 || $assetId === null) { + if ($assetId === null) { + return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]); + } + if ($itemId <= 0) { return $this->error(__('item_id and user_id/session_id required')); } @@ -963,11 +969,21 @@ class Playx extends Api } $itemId = intval($request->post('item_id', 0)); + $addressId = intval($request->post('address_id', 0)); $assetId = $this->resolvePlayxAssetIdFromRequest($request); - $receiverName = $request->post('receiver_name', ''); - $receiverPhone = $request->post('receiver_phone', ''); - $receiverAddress = $request->post('receiver_address', ''); - if ($itemId <= 0 || $assetId === null || $receiverName === '' || $receiverPhone === '' || $receiverAddress === '') { + if ($itemId <= 0 || $addressId <= 0) { + return $this->error(__('Missing required fields')); + } + if ($assetId === null) { + return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]); + } + + $addrRow = MallAddress::where('id', $addressId)->where('playx_user_asset_id', $assetId)->find(); + if (!$addrRow) { + return $this->error(__('Shipping address not found')); + } + $snapshot = MallAddress::snapshotForPhysicalOrder($addrRow); + if ($snapshot['receiver_phone'] === '' || $snapshot['receiver_address'] === '' || $snapshot['receiver_name'] === '') { return $this->error(__('Missing required fields')); } @@ -996,9 +1012,10 @@ class Playx extends Api 'status' => MallOrder::STATUS_PENDING, 'mall_item_id' => $item->id, 'points_cost' => $item->score, - 'receiver_name' => $receiverName, - 'receiver_phone' => $receiverPhone, - 'receiver_address' => $receiverAddress, + 'mall_address_id' => $addressId, + 'receiver_name' => $snapshot['receiver_name'], + 'receiver_phone' => $snapshot['receiver_phone'], + 'receiver_address' => $snapshot['receiver_address'], 'create_time' => time(), 'update_time' => time(), ]); @@ -1026,7 +1043,10 @@ class Playx extends Api $itemId = intval($request->post('item_id', 0)); $assetId = $this->resolvePlayxAssetIdFromRequest($request); - if ($itemId <= 0 || $assetId === null) { + if ($assetId === null) { + return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]); + } + if ($itemId <= 0) { return $this->error(__('item_id and user_id/session_id required')); } diff --git a/app/api/lang/en.php b/app/api/lang/en.php index bd6dc3c..d335411 100644 --- a/app/api/lang/en.php +++ b/app/api/lang/en.php @@ -22,6 +22,7 @@ return [ 'Temp login is disabled' => 'Temp login is disabled', 'Failed to create temp account' => 'Failed to allocate a unique phone number, please retry later', 'Parameter username can not be empty' => 'Parameter username can not be empty', + 'Token expiration' => 'Session expired, please login again.', // Member center account 'Data updated successfully~' => 'Data updated successfully~', 'Password has been changed~' => 'Password has been changed~', @@ -49,6 +50,7 @@ return [ 'Insufficient points' => 'Insufficient points', 'Redeem submitted, please wait about 10 minutes' => 'Redeem submitted, please wait about 10 minutes', 'Missing required fields' => 'Missing required fields', + 'Shipping address not found' => 'Shipping address not found', 'Out of stock' => 'Out of stock', 'Redeem success' => 'Redeem successful', 'Withdraw submitted, please wait about 10 minutes' => 'Withdrawal submitted, please wait about 10 minutes', diff --git a/app/api/lang/zh-cn.php b/app/api/lang/zh-cn.php index 3599b01..37bbd8a 100644 --- a/app/api/lang/zh-cn.php +++ b/app/api/lang/zh-cn.php @@ -82,6 +82,8 @@ return [ 'Redeem submitted, please wait about 10 minutes' => '兑换已提交,请等待约 10 分钟', 'Missing required fields' => '缺少必填字段', 'Out of stock' => '库存不足', + 'Record not found' => '记录不存在', + 'Shipping address not found' => '收货地址不存在', 'Redeem success' => '兑换成功', 'Withdraw submitted, please wait about 10 minutes' => '提现申请已提交,请等待约 10 分钟', ]; \ No newline at end of file diff --git a/app/common/model/MallAddress.php b/app/common/model/MallAddress.php index c645429..1c3a75b 100644 --- a/app/common/model/MallAddress.php +++ b/app/common/model/MallAddress.php @@ -41,7 +41,18 @@ class MallAddress extends Model public function getregionTextAttr($value, $row): string { if ($row['region'] === '' || $row['region'] === null) return ''; - $cityNames = \support\think\Db::name('area')->whereIn('id', $row['region'])->column('name'); + $region = $row['region']; + $ids = $region; + if (!is_array($ids)) { + $ids = explode(',', (string) $ids); + } + $ids = array_values(array_filter(array_map('trim', $ids), static function ($s) { + return $s !== ''; + })); + if (empty($ids)) { + return ''; + } + $cityNames = \support\think\Db::name('area')->whereIn('id', $ids)->column('name'); return $cityNames ? implode(',', $cityNames) : ''; } @@ -49,4 +60,27 @@ class MallAddress extends Model { return $this->belongsTo(\app\common\model\MallUserAsset::class, 'playx_user_asset_id', 'id'); } + + /** + * 实物订单收货快照(写入 mall_order.receiver_*,与 mall_address 当前内容一致) + * + * @return array{receiver_name: string, receiver_phone: string, receiver_address: string} + */ + public static function snapshotForPhysicalOrder(self $addr): array + { + $regionText = $addr->region_text ?? ''; + $parts = array_filter([ + trim($regionText), + trim($addr->detail_address ?? ''), + ], static function ($s) { + return $s !== ''; + }); + $receiverAddress = implode(' ', $parts); + + return [ + 'receiver_name' => trim($addr->receiver_name ?? ''), + 'receiver_phone' => trim($addr->phone ?? ''), + 'receiver_address' => $receiverAddress, + ]; + } } \ No newline at end of file diff --git a/app/common/model/MallOrder.php b/app/common/model/MallOrder.php index b5ec6fa..3e2ebb8 100644 --- a/app/common/model/MallOrder.php +++ b/app/common/model/MallOrder.php @@ -28,6 +28,7 @@ use support\think\Model; * @property string $receiver_name * @property string $receiver_phone * @property string|null $receiver_address + * @property int|null $mall_address_id */ class MallOrder extends Model { @@ -56,12 +57,18 @@ class MallOrder extends Model 'points_cost' => 'integer', 'amount' => 'float', 'multiplier' => 'integer', - 'retry_count' => 'integer', + 'retry_count' => 'integer', + 'mall_address_id' => 'integer', ]; public function mallItem(): \think\model\relation\BelongsTo { return $this->belongsTo(MallItem::class, 'mall_item_id', 'id'); } + + public function mallAddress(): \think\model\relation\BelongsTo + { + return $this->belongsTo(MallAddress::class, 'mall_address_id', 'id'); + } } diff --git a/app/common/model/MallUserAsset.php b/app/common/model/MallUserAsset.php index dd90c6a..bbbafa7 100644 --- a/app/common/model/MallUserAsset.php +++ b/app/common/model/MallUserAsset.php @@ -31,18 +31,21 @@ class MallUserAsset extends Model */ public static function ensureForUsername(string $username): self { -// $username = trim($username); + $username = trim($username); $existing = self::where('username', $username)->find(); if ($existing) { return $existing; } - //手机号直接=用户名,暂时不做验证 -// $phone = self::allocateUniquePhone(); + // 创建用户时:phone 与 username 同值(H5 临时账号) $phone = $username; - - if ($phone === null) { - throw new \RuntimeException('Failed to allocate unique phone'); + $phoneExisting = self::where('phone', $phone)->find(); + if ($phoneExisting) { + // 若历史数据存在“手机号=用户名”的行,直接复用;若不一致则拒绝创建,避免污染 + if (trim((string) ($phoneExisting->username ?? '')) === $username) { + return $phoneExisting; + } + throw new \RuntimeException('Username is already used by another account'); } $pwd = hash_password(Random::build('alnum', 16)); @@ -77,16 +80,6 @@ class MallUserAsset extends Model return $created; } - private static function allocateUniquePhone(): ?string - { - for ($i = 0; $i < 8; $i++) { - $candidate = '13' . sprintf('%09d', mt_rand(0, 999999999)); - if (!self::where('phone', $candidate)->find()) { - return $candidate; - } - } - - return null; - } + // allocateUniquePhone 已废弃:临时登录场景下 phone=用户名 } diff --git a/config/route.php b/config/route.php index 8c77845..d1d8f3d 100644 --- a/config/route.php +++ b/config/route.php @@ -113,19 +113,19 @@ Route::get('/api/v1/authToken', [\app\api\controller\v1\Auth::class, 'authToken' Route::get('/api/v1/temLogin', [\app\api\controller\v1\Auth::class, 'temLogin']); // api/v1 PlayX 积分商城 -Route::post('/api/v1/playx/daily-push', [\app\api\controller\v1\Playx::class, 'dailyPush']); -Route::post('/api/v1/playx/verify-token', [\app\api\controller\v1\Playx::class, 'verifyToken']); -Route::get('/api/v1/playx/assets', [\app\api\controller\v1\Playx::class, 'assets']); -Route::post('/api/v1/playx/claim', [\app\api\controller\v1\Playx::class, 'claim']); -Route::get('/api/v1/playx/items', [\app\api\controller\v1\Playx::class, 'items']); -Route::post('/api/v1/playx/bonus/redeem', [\app\api\controller\v1\Playx::class, 'bonusRedeem']); -Route::post('/api/v1/playx/physical/redeem', [\app\api\controller\v1\Playx::class, 'physicalRedeem']); -Route::post('/api/v1/playx/withdraw/apply', [\app\api\controller\v1\Playx::class, 'withdrawApply']); -Route::get('/api/v1/playx/orders', [\app\api\controller\v1\Playx::class, 'orders']); -Route::get('/api/v1/playx/address/list', [\app\api\controller\v1\Playx::class, 'addressList']); -Route::post('/api/v1/playx/address/add', [\app\api\controller\v1\Playx::class, 'addressAdd']); -Route::post('/api/v1/playx/address/edit', [\app\api\controller\v1\Playx::class, 'addressEdit']); -Route::post('/api/v1/playx/address/delete', [\app\api\controller\v1\Playx::class, 'addressDelete']); +Route::post('/api/v1/mall/dailyPush', [\app\api\controller\v1\Playx::class, 'dailyPush']); +Route::post('/api/v1/mall/verifyToken', [\app\api\controller\v1\Playx::class, 'verifyToken']); +Route::get('/api/v1/mall/assets', [\app\api\controller\v1\Playx::class, 'assets']); +Route::post('/api/v1/mall/claim', [\app\api\controller\v1\Playx::class, 'claim']); +Route::get('/api/v1/mall/items', [\app\api\controller\v1\Playx::class, 'items']); +Route::post('/api/v1/mall/bonusRedeem', [\app\api\controller\v1\Playx::class, 'bonusRedeem']); +Route::post('/api/v1/mall/physicalRedeem', [\app\api\controller\v1\Playx::class, 'physicalRedeem']); +Route::post('/api/v1/mall/withdrawApply', [\app\api\controller\v1\Playx::class, 'withdrawApply']); +Route::get('/api/v1/mall/orders', [\app\api\controller\v1\Playx::class, 'orders']); +Route::get('/api/v1/mall/addressList', [\app\api\controller\v1\Playx::class, 'addressList']); +Route::post('/api/v1/mall/addressAdd', [\app\api\controller\v1\Playx::class, 'addressAdd']); +Route::post('/api/v1/mall/addressEdit', [\app\api\controller\v1\Playx::class, 'addressEdit']); +Route::post('/api/v1/mall/addressDelete', [\app\api\controller\v1\Playx::class, 'addressDelete']); // ==================== Admin 路由 ==================== // Admin 多为 JSON API,前端可能用 GET 传参查列表、POST 提交表单,使用 any 确保兼容 diff --git a/docs/H5-积分商城接口文档.md b/docs/H5-积分商城接口文档.md new file mode 100644 index 0000000..88f5d54 --- /dev/null +++ b/docs/H5-积分商城接口文档.md @@ -0,0 +1,205 @@ +# H5 积分商城接口文档(含流程说明) + +> 面向:H5(活动页/积分商城前台)调用 +> 基础路径:`/api/v1` +> 返回结构:BuildAdmin 通用 `code/msg/time/data`(成功 `code=1`) + +--- + +## 1. 总体流程说明 + +### 1.1 流程 A:H5 临时登录(推荐) + +适用场景:H5 只需要“用户名级别”的轻量登录,不依赖 PlayX 的 token。 + +1. H5 调用 `GET/POST /api/v1/temLogin?username=xxx` 获取 **商城 token**(类型 `muser`) +2. H5 后续请求统一携带该 token(推荐放在 Header:`token: `,也可用参数 `token`) +3. H5 调用: + - `GET /api/v1/mall/assets` 获取资产 + - `POST /api/v1/mall/claim` 领取积分(幂等) + - `GET /api/v1/mall/items` 获取商品 + - `POST /api/v1/mall/bonusRedeem` / `physicalRedeem` / `withdrawApply` 提交兑换/提现 + - `GET /api/v1/mall/orders` 查询订单 + - `GET/POST /api/v1/mall/address*` 管理地址(addressList/addressAdd/addressEdit/addressDelete) + +### 1.2 流程 B:PlayX token 换取 session(兼容) + +适用场景:H5 已经拿到了 PlayX 下发的 token,希望换取商城侧 `session_id`。 + +1. H5 调用 `POST /api/v1/mall/verifyToken`(传 `token` 或 `session`) +2. 服务端返回 `data.session_id` +3. H5 后续请求携带 `session_id`(优先级高于 token) + +--- + +## 2. 身份与鉴权(重要) + +以下接口都会通过服务端逻辑解析“当前资产主体”,优先级如下: + +1. **`session_id`**(GET/POST):对应表 `mall_session`,未过期则可映射到资产主体 +2. **`token`**(GET/POST 或 Header):支持会员 token 或 `muser` token(H5 临时登录签发) +3. **`user_id`**(GET/POST): + - 纯数字:视为 `mall_user_asset.id` + - 非纯数字:按 `mall_user_asset.playx_user_id` 查找 + +推荐做法: +- H5 统一只用 **Header `token`**(值为 `muser` token),避免 URL 泄露与参数歧义。 + +--- + +## 3. 接口列表(H5 常用) + +### 3.1 H5 临时登录 + +**GET/POST** ` /api/v1/temLogin ` + +参数: +- `username`:必填,用户名(字符串) + +成功返回 `data.userInfo`: +- `id`:资产主键(`mall_user_asset.id`) +- `username`:用户名 +- `playx_user_id`:映射的 PlayX 用户标识(字符串) +- `token`:**muser token**(后续请求使用) +- `refresh_token`:刷新 token(当前前端未强依赖可不接) +- `expires_in`:秒 + +示例: + +```bash +curl "https://{域名}/api/v1/temLogin?username=test001" +``` + +--- + +### 3.2 资产查询 + +**GET** ` /api/v1/mall/assets ` + +鉴权:携带 `token` 或 `session_id` 或 `user_id` + +成功返回 `data`: +- `locked_points`:待领取积分 +- `available_points`:可用积分 +- `today_limit`:今日可领取上限 +- `today_claimed`:今日已领取 +- `withdrawable_cash`:可提现现金(积分按配置比例换算) + +--- + +### 3.3 领取积分(幂等) + +**POST** ` /api/v1/mall/claim ` + +参数: +- `claim_request_id`:必填,幂等键(建议:`{业务前缀}_{assetId}_{毫秒时间戳}`) +- 身份参数:`token` / `session_id` / `user_id` 三选一(推荐 `token`) + +说明: +- 同一个 `claim_request_id` 重复提交会直接返回成功(不会重复入账) +- 会受 `today_limit/today_claimed/locked_points` 限制 + +--- + +### 3.4 商品列表 + +**GET** ` /api/v1/mall/items ` + +参数: +- `type`:可选,`BONUS | PHYSICAL | WITHDRAW` + +--- + +### 3.5 红利兑换(提交订单) + +**POST** ` /api/v1/mall/bonusRedeem ` + +参数: +- `item_id`:必填 +- 身份参数:`token` / `session_id` / `user_id` + +返回: +- `data.order_id` +- `data.status`(通常 `PENDING`) + +--- + +### 3.6 实物兑换(提交订单) + +**POST** ` /api/v1/mall/physicalRedeem ` + +参数: +- `item_id`:必填 +- `address_id`:必填,收货地址主键(`mall_address.id`,须为当前用户资产下地址) +- 身份参数:`token` / `session_id` / `user_id` + +说明:服务端会将该地址在下单时刻的 **收货人 / 电话 / 完整地址** 写入订单字段(快照),并写入 `mall_order.mall_address_id` 关联所选地址。 + +--- + +### 3.7 提现申请(提交订单) + +**POST** ` /api/v1/mall/withdrawApply ` + +参数: +- `item_id`:必填 +- 身份参数:`token` / `session_id` / `user_id` + +--- + +### 3.8 订单列表 + +**GET** ` /api/v1/mall/orders ` + +鉴权:`token` / `session_id` / `user_id` + +说明: +- 返回最多 100 条 +- 订单里包含 `mallItem`(商品信息) + +--- + +## 4. 地址管理(H5) + +> 地址与资产主体通过 `playx_user_asset_id` 关联(即 `mall_user_asset.id`)。 + +### 4.1 地址列表 +**GET** ` /api/v1/mall/addressList ` + +### 4.2 新增地址 +**POST** ` /api/v1/mall/addressAdd ` + +Body 含 `receiver_name`(收货人,建议填写;实物兑换下单快照需要非空的收货人、电话与拼接后的完整地址)。 + +### 4.3 编辑地址 +**POST** ` /api/v1/mall/addressEdit ` + +### 4.4 删除地址 +**POST** ` /api/v1/mall/addressDelete ` + +--- + +## 5. session 换取(可选) + +### 5.1 token 换 session + +**POST** ` /api/v1/mall/verifyToken ` + +参数(二选一): +- `token` +- `session` + +成功返回: +- `data.session_id` +- `data.user_id` +- `data.username` +- `data.token_expire_at` + +--- + +## 6. 常见错误与排查 + +- **401 登录态过期**:token/session 过期或不匹配;请重新 `temLogin` 或重新 `verifyToken` +- **提示缺少必填字段**:按各接口参数补齐(如 `claim_request_id`、`item_id`、`address_id`(实物)、地址中收货人/电话/完整地址等) +- **积分不足/无可领取积分**:`locked_points<=0` 或已达 `today_limit` + diff --git a/docs/PlayX-接口文档.md b/docs/PlayX-接口文档.md index 2b09c0e..77efc45 100644 --- a/docs/PlayX-接口文档.md +++ b/docs/PlayX-接口文档.md @@ -13,7 +13,7 @@ ### 1.1 Daily Push API * 方法:`POST` -* 路径:`/api/v1/playx/daily-push` +* 路径:`/api/v1/mall/dailyPush` #### Header(多语言,可选) - `lang`: `zh`/`zh-cn` 返回中文(默认);`en` 返回英文 @@ -25,7 +25,7 @@ - `X-Signature`:签名(HMAC_SHA256) 服务端签名计算: -- `canonical = X-Timestamp + "\n" + X-Request-Id + "\nPOST\n/api/v1/playx/daily-push\n" + sha256(json_body)` +- `canonical = X-Timestamp + "\n" + X-Request-Id + "\nPOST\n/api/v1/mall/dailyPush\n" + sha256(json_body)` - `expected = hash_hmac('sha256', canonical, daily_push_secret)` - 校验:`hash_equals(expected, X-Signature)` @@ -104,7 +104,7 @@ #### 示例(未开启签名校验) 请求: ```bash -curl -X POST 'http://localhost:1818/api/v1/playx/daily-push' \ +curl -X POST 'http://localhost:1818/api/v1/mall/dailyPush' \ -H 'Content-Type: application/json' \ -d '{ "request_id":"req_1001", @@ -136,7 +136,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/daily-push' \ #### 示例(新版批量上报) 请求: ```bash -curl -X POST 'http://localhost:1818/api/v1/playx/daily-push' \ +curl -X POST 'http://localhost:1818/api/v1/mall/dailyPush' \ -H 'Content-Type: application/json' \ -d '{ "report_date": "1700000000", @@ -181,7 +181,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/daily-push' \ ## 2. PlayX -> 积分商城(商城调用 PlayX) > 下面这些接口由 PlayX 提供。商城侧仅按“请求参数 + 期望返回判定条件”发起调用与处理结果。 -> **说明**:H5 调商城的 **`/api/v1/playx/verify-token`** 在配置 **`playx.verify_token_local_only=true`**(默认)时**不会请求**本节接口,而是在商城内校验 `muser` token;远程对接 PlayX 时见 **3.3** 与下文 **2.1**。 +> **说明**:H5 调商城的 **`/api/v1/mall/verifyToken`** 在配置 **`playx.verify_token_local_only=true`**(默认)时**不会请求**本节接口,而是在商城内校验 `muser` token;远程对接 PlayX 时见 **3.3** 与下文 **2.1**。 ### 2.1 Token Verification API(PlayX 侧实现,远程验证时使用) * 方法:`POST` @@ -406,7 +406,7 @@ curl -G 'http://localhost:1818/api/v1/temLogin' --data-urlencode 'username=demo_ ### 3.3 Token 验证(换 session) * 方法:`POST`(推荐 `GET` 传 `token` 亦可) -* 路径:`/api/v1/playx/verify-token` +* 路径:`/api/v1/mall/verifyToken` #### 配置:本地验证 vs 远程 PlayX @@ -442,7 +442,7 @@ curl -G 'http://localhost:1818/api/v1/temLogin' --data-urlencode 'username=demo_ #### 示例(本地验证) ```bash -curl -X POST 'http://localhost:1818/api/v1/playx/verify-token' \ +curl -X POST 'http://localhost:1818/api/v1/mall/verifyToken' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'token=上一步TemLogin返回的token' ``` @@ -455,32 +455,32 @@ curl -X POST 'http://localhost:1818/api/v1/playx/verify-token' \ #### 3.x.1 地址列表 * 方法:`GET` -* 路径:`/api/v1/playx/address/list` +* 路径:`/api/v1/mall/addressList` 返回:`data.list` 为地址数组。 #### 3.x.2 添加地址 * 方法:`POST` -* 路径:`/api/v1/playx/address/add` +* 路径:`/api/v1/mall/addressAdd` Body: | 字段 | 必填 | 说明 | |------|------|------| +| `receiver_name` | 是 | 收货人 | | `phone` | 是 | 电话 | | `region` | 是 | 地区(数组或逗号分隔字符串) | | `detail_address` | 是 | 详细地址 | -| `address` | 是 | 地址补充 | | `default_setting` | 否 | `1` 设为默认地址 | #### 3.x.3 修改地址(含设为默认) * 方法:`POST` -* 路径:`/api/v1/playx/address/edit` +* 路径:`/api/v1/mall/addressEdit` Body:`id` 必填,其余字段按需传入更新。 #### 3.x.4 删除地址 * 方法:`POST` -* 路径:`/api/v1/playx/address/delete` +* 路径:`/api/v1/mall/addressDelete` Body:`id` 必填。若删除默认地址,服务端会自动挑选一条剩余地址设为默认(如存在)。 @@ -519,7 +519,7 @@ Body:`id` 必填。若删除默认地址,服务端会自动挑选一条剩 ### 3.4 用户资产(Assets) * 方法:`GET` -* 路径:`/api/v1/playx/assets` +* 路径:`/api/v1/mall/assets` #### 请求参数(鉴权) @@ -541,7 +541,7 @@ Body:`id` 必填。若删除默认地址,服务端会自动挑选一条剩 #### 示例 ```bash -curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'token=上一步temLogin返回的token' +curl -G 'http://localhost:1818/api/v1/mall/assets' --data-urlencode 'token=上一步temLogin返回的token' ``` 响应(示例): @@ -564,7 +564,7 @@ curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'token=上 ### 3.5 领取(Claim) * 方法:`POST` -* 路径:`/api/v1/playx/claim` +* 路径:`/api/v1/mall/claim` #### 请求 Body 必填: @@ -581,7 +581,7 @@ curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'token=上 #### 示例 ```bash -curl -X POST 'http://localhost:1818/api/v1/playx/claim' \ +curl -X POST 'http://localhost:1818/api/v1/mall/claim' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'claim_request_id=claim_001' \ --data-urlencode 'token=上一步temLogin返回的token' @@ -623,7 +623,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/claim' \ ### 3.6 商品列表 * 方法:`GET` -* 路径:`/api/v1/playx/items` +* 路径:`/api/v1/mall/items` #### 请求参数 * `type`(可选):`BONUS` | `PHYSICAL` | `WITHDRAW` @@ -636,7 +636,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/claim' \ #### 示例 请求: ```bash -curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=WITHDRAW' +curl -G 'http://localhost:1818/api/v1/mall/items' --data-urlencode 'type=WITHDRAW' ``` 响应(示例): @@ -666,7 +666,7 @@ curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=WITHDR ### 3.7 红利兑换(Bonus Redeem) * 方法:`POST` -* 路径:`/api/v1/playx/bonus/redeem` +* 路径:`/api/v1/mall/bonusRedeem` #### 请求 Body 必填: @@ -680,7 +680,7 @@ curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=WITHDR #### 示例 ```bash -curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \ +curl -X POST 'http://localhost:1818/api/v1/mall/bonusRedeem' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'item_id=123' \ --data-urlencode 'session_id=7b1c....' @@ -703,14 +703,12 @@ curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \ ### 3.8 实物兑换(Physical Redeem) * 方法:`POST` -* 路径:`/api/v1/playx/physical/redeem` +* 路径:`/api/v1/mall/physicalRedeem` #### 请求 Body 必填: * `item_id`:商品 ID(要求 `mall_item.type=PHYSICAL` 且 `status=1`) -* `receiver_name`:收货人 -* `receiver_phone`:收货电话 -* `receiver_address`:收货地址 +* `address_id`:收货地址 ID(`mall_address.id`,须属于当前用户资产;下单时写入 `mall_order.mall_address_id`,并将该地址快照写入 `receiver_name` / `receiver_phone` / `receiver_address`) 鉴权:同 **3.1**(`session_id` / `token` / `user_id`) #### 返回(成功) @@ -719,12 +717,10 @@ curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \ #### 示例 ```bash -curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \ +curl -X POST 'http://localhost:1818/api/v1/mall/physicalRedeem' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'item_id=200' \ - --data-urlencode 'receiver_name=张三' \ - --data-urlencode 'receiver_phone=18800001111' \ - --data-urlencode 'receiver_address=北京市朝阳区XX路XX号' \ + --data-urlencode 'address_id=10' \ --data-urlencode 'session_id=7b1c....' ``` @@ -742,7 +738,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \ ### 3.9 提现申请(Withdraw Apply) * 方法:`POST` -* 路径:`/api/v1/playx/withdraw/apply` +* 路径:`/api/v1/mall/withdrawApply` #### 请求 Body 必填: @@ -756,7 +752,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \ #### 示例 ```bash -curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \ +curl -X POST 'http://localhost:1818/api/v1/mall/withdrawApply' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'item_id=321' \ --data-urlencode 'session_id=7b1c....' @@ -779,7 +775,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \ ### 3.10 订单列表 * 方法:`GET` -* 路径:`/api/v1/playx/orders` +* 路径:`/api/v1/mall/orders` #### 请求参数(鉴权) @@ -793,7 +789,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \ #### 示例 请求: ```bash -curl -G 'http://localhost:1818/api/v1/playx/orders' --data-urlencode 'token=上一步temLogin返回的token' +curl -G 'http://localhost:1818/api/v1/mall/orders' --data-urlencode 'token=上一步temLogin返回的token' ``` 响应(示例,简化): @@ -828,5 +824,5 @@ curl -G 'http://localhost:1818/api/v1/playx/orders' --data-urlencode 'token=上 --- ### 3.11 同步额度(可选) -当前代码未实现并未注册路由:`/api/v1/playx/sync-limit`。 +当前代码未实现并未注册路由:`/api/v1/mall/syncLimit`。 diff --git a/docs/PlayX-调用积分商城接口说明.md b/docs/PlayX-调用积分商城接口说明.md index cf90ef5..8bf0c1b 100644 --- a/docs/PlayX-调用积分商城接口说明.md +++ b/docs/PlayX-调用积分商城接口说明.md @@ -12,7 +12,7 @@ 将下列路径拼在积分商城对外域名之后,例如: -`https://{商城域名}/api/v1/playx/daily-push` +`https://{商城域名}/api/v1/mall/dailyPush` (联调时请向商城方索取正式环境与测试环境地址。) @@ -78,7 +78,7 @@ sequenceDiagram participant M as 积分商城 Note over PX,M: 按约定配置 HMAC 密钥 - PX->>M: POST /api/v1/playx/daily-push(JSON Body) + PX->>M: POST /api/v1/mall/dailyPush(JSON Body) M-->>PX: code=1, data.accepted / deduped ``` @@ -88,14 +88,14 @@ sequenceDiagram ### 2.2 用户侧(H5 / 内嵌页)→ 商城:会话与业务接口 -以下接口多由 **用户在浏览器内**打开积分商城 H5 后调用,通过 **`session_id`**(先调 `verify-token` 获取)或 **`token`**(商城 `muser` 类 token)标识用户,**不一定由 PlayX 后端直接调用**: +以下接口多由 **用户在浏览器内**打开积分商城 H5 后调用,通过 **`session_id`**(先调 `verifyToken` 获取)或 **`token`**(商城 `muser` 类 token)标识用户,**不一定由 PlayX 后端直接调用**: -- `POST /api/v1/playx/verify-token`:用 PlayX token 换商城 `session_id` -- `GET /api/v1/playx/assets`:查询资产 -- `POST /api/v1/playx/claim`:领取积分 -- `GET /api/v1/playx/items`:商品列表 -- `POST /api/v1/playx/bonus/redeem` / `physical/redeem` / `withdraw/apply`:兑换与提现申请 -- `GET /api/v1/playx/orders`:订单列表 +- `POST /api/v1/mall/verifyToken`:用 PlayX token 换商城 `session_id` +- `GET /api/v1/mall/assets`:查询资产 +- `POST /api/v1/mall/claim`:领取积分 +- `GET /api/v1/mall/items`:商品列表 +- `POST /api/v1/mall/bonusRedeem` / `physicalRedeem` / `withdrawApply`:兑换与提现申请 +- `GET /api/v1/mall/orders`:订单列表 若 PlayX 后端需要代替用户调用上述接口,须同样携带有效的 `session_id` 或 `token`,并遵守同一用户身份规则(见 **§4 身份说明**)。 @@ -112,7 +112,7 @@ sequenceDiagram | 项目 | 值 | |------|-----| | 方法 | `POST` | -| 路径 | `/api/v1/playx/daily-push` | +| 路径 | `/api/v1/mall/dailyPush` | ### 3.2 鉴权(按商城部署配置,可组合) @@ -133,7 +133,7 @@ sequenceDiagram 签名原文与计算: ``` -canonical = X-Timestamp + "\n" + X-Request-Id + "\nPOST\n/api/v1/playx/daily-push\n" + sha256(json_body) +canonical = X-Timestamp + "\n" + X-Request-Id + "\nPOST\n/api/v1/mall/dailyPush\n" + sha256(json_body) expected = HMAC_SHA256( canonical , PLAYX_DAILY_PUSH_SECRET ) ``` @@ -150,7 +150,7 @@ expected = HMAC_SHA256( canonical , PLAYX_DAILY_PUSH_SECRET ) ### 3.3 Body 参数(JSON) -`/api/v1/playx/daily-push` 支持 **两种入参格式**(按字段自动识别): +`/api/v1/mall/dailyPush` 支持 **两种入参格式**(按字段自动识别): #### 格式 A:旧版单条上报(兼容) | 字段 | 类型 | 必填 | 说明 | @@ -215,7 +215,7 @@ expected = HMAC_SHA256( canonical , PLAYX_DAILY_PUSH_SECRET ) ### 3.6 请求示例 ```bash -curl -X POST 'https://{商城域名}/api/v1/playx/daily-push' \ +curl -X POST 'https://{商城域名}/api/v1/mall/dailyPush' \ -H 'Content-Type: application/json' \ -H 'X-Request-Id: req_1700000000_123456' \ -H 'X-Timestamp: 1700000000' \ @@ -279,7 +279,7 @@ curl -X POST 'https://{商城域名}/api/v1/playx/daily-push' \ > 下列均为 **BuildAdmin 通用 `code/msg/time/data` 结构**;成功时 `code=1`。 -### 5.1 `POST /api/v1/playx/verify-token` +### 5.1 `POST /api/v1/mall/verifyToken` 用于将 **PlayX token**(或本地联调 token)换 **商城 `session_id`**。 @@ -300,7 +300,7 @@ curl -X POST 'https://{商城域名}/api/v1/playx/daily-push' \ --- -### 5.2 `GET /api/v1/playx/assets` +### 5.2 `GET /api/v1/mall/assets` 查询积分资产;需 **§4** 身份。 @@ -316,7 +316,7 @@ curl -X POST 'https://{商城域名}/api/v1/playx/daily-push' \ --- -### 5.3 `POST /api/v1/playx/claim` +### 5.3 `POST /api/v1/mall/claim` 领取积分;需 **§4** 身份。 @@ -328,7 +328,7 @@ curl -X POST 'https://{商城域名}/api/v1/playx/daily-push' \ --- -### 5.4 `GET /api/v1/playx/items` +### 5.4 `GET /api/v1/mall/items` 商品列表。 @@ -340,7 +340,7 @@ curl -X POST 'https://{商城域名}/api/v1/playx/daily-push' \ --- -### 5.5 `POST /api/v1/playx/bonus/redeem` +### 5.5 `POST /api/v1/mall/bonusRedeem` 红利兑换;需 **§4** 身份。 @@ -352,20 +352,18 @@ curl -X POST 'https://{商城域名}/api/v1/playx/daily-push' \ --- -### 5.6 `POST /api/v1/playx/physical/redeem` +### 5.6 `POST /api/v1/mall/physicalRedeem` 实物兑换;需 **§4** 身份。 -| 参数 | 必填 | -|------|------| -| `item_id` | 是 | -| `receiver_name` | 是 | -| `receiver_phone` | 是 | -| `receiver_address` | 是 | +| 参数 | 必填 | 说明 | +|------|------|------| +| `item_id` | 是 | 实物商品 ID | +| `address_id` | 是 | `mall_address.id`(当前用户下地址);订单保存 `mall_address_id` 与地址快照 | --- -### 5.7 `POST /api/v1/playx/withdraw/apply` +### 5.7 `POST /api/v1/mall/withdrawApply` 提现类兑换申请;需 **§4** 身份。 @@ -375,7 +373,7 @@ curl -X POST 'https://{商城域名}/api/v1/playx/daily-push' \ --- -### 5.8 `GET /api/v1/playx/orders` +### 5.8 `GET /api/v1/mall/orders` 订单列表;需 **§4** 身份。 @@ -389,40 +387,40 @@ curl -X POST 'https://{商城域名}/api/v1/playx/daily-push' \ #### 5.9.1 获取收货地址列表 - **方法**:`GET` -- **路径**:`/api/v1/playx/address/list` +- **路径**:`/api/v1/mall/addressList` 返回 `data.list`:地址数组(按 `default_setting` 优先,其次 id 倒序)。 #### 5.9.2 添加收货地址 - **方法**:`POST` -- **路径**:`/api/v1/playx/address/add` +- **路径**:`/api/v1/mall/addressAdd` Body(表单或 JSON 均可,建议 JSON): | 字段 | 必填 | 说明 | |------|------|------| +| `receiver_name` | 是 | 收货人 | | `phone` | 是 | 联系电话 | | `region` | 是 | 地区(可传数组或逗号分隔字符串) | | `detail_address` | 是 | 详细地址(短文本) | -| `address` | 是 | 地址补充(可长文本) | | `default_setting` | 否 | `1` 设为默认地址;`0` 或不传为非默认 | 成功返回:`data.id` 为新地址 id。 #### 5.9.3 修改收货地址(含设置默认) - **方法**:`POST` -- **路径**:`/api/v1/playx/address/edit` +- **路径**:`/api/v1/mall/addressEdit` Body: | 字段 | 必填 | 说明 | |------|------|------| | `id` | 是 | 地址 id | -| `phone/region/detail_address/address/default_setting` | 否 | 需要修改的字段(只更新传入项) | +| `receiver_name/phone/region/detail_address/default_setting` | 否 | 需要修改的字段(只更新传入项) | 当 `default_setting=1`:会自动把该用户其他地址的 `default_setting` 置为 0。 #### 5.9.4 删除收货地址 - **方法**:`POST` -- **路径**:`/api/v1/playx/address/delete` +- **路径**:`/api/v1/mall/addressDelete` Body: | 字段 | 必填 | 说明 | @@ -438,7 +436,7 @@ Body: | 环境变量 / 配置 | 作用 | |-----------------|------| | `PLAYX_DAILY_PUSH_SECRET` | 非空则 Daily Push 必须带合法 HMAC 头 | -| `PLAYX_VERIFY_TOKEN_LOCAL_ONLY` | 为 true 时 verify-token 不请求 PlayX 远程 | +| `PLAYX_VERIFY_TOKEN_LOCAL_ONLY` | 为 true 时 verifyToken 不请求 PlayX 远程 | | `PLAYX_API_BASE_URL` | 商城调用 PlayX 接口时使用(与「PlayX 调商城」方向相反) | --- diff --git a/docs/积分商城-PlayX对接实施方案.md b/docs/积分商城-PlayX对接实施方案.md index cf8fd33..693071b 100644 --- a/docs/积分商城-PlayX对接实施方案.md +++ b/docs/积分商城-PlayX对接实施方案.md @@ -11,7 +11,7 @@ 接收 PlayX 每日 T+1 数据推送。 * 方法:`POST` -* 路径:`/api/v1/playx/daily-push` +* 路径:`/api/v1/mall/dailyPush` ##### 请求(Header) 当配置了 `playx.daily_push_secret`(Daily Push 签名校验)时,需要携带: @@ -20,7 +20,7 @@ * `X-Signature`:签名(HMAC_SHA256) 签名计算逻辑(服务端): -* canonical:`{X-Timestamp}\n{X-Request-Id}\nPOST\n/api/v1/playx/daily-push\n{sha256(json_body)}` +* canonical:`{X-Timestamp}\n{X-Request-Id}\nPOST\n/api/v1/mall/dailyPush\n{sha256(json_body)}` * expected:`hash_hmac('sha256', canonical, daily_push_secret)` ##### 请求(Body) @@ -51,7 +51,7 @@ ##### 示例 无签名校验(`PLAYX_DAILY_PUSH_SECRET` 为空): ```bash -curl -X POST 'http://localhost:1818/api/v1/playx/daily-push' \ +curl -X POST 'http://localhost:1818/api/v1/mall/dailyPush' \ -H 'Content-Type: application/json' \ -d '{ "request_id":"req_1001", @@ -246,7 +246,7 @@ curl -G '${playx.api.base_url}/api/v1/transaction/status' \ ### 1.3 商城内部 API(供 H5 前端调用) #### Token 验证 * 方法:`POST` -* 路径:`/api/v1/playx/verify-token` +* 路径:`/api/v1/mall/verifyToken` 请求(Body): * `token`(必填,优先读取) @@ -260,14 +260,14 @@ curl -G '${playx.api.base_url}/api/v1/transaction/status' \ 示例: ```bash -curl -X POST 'http://localhost:1818/api/v1/playx/verify-token' \ +curl -X POST 'http://localhost:1818/api/v1/mall/verifyToken' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'token=PLAYX_TOKEN_XXX' ``` #### 用户资产 * 方法:`GET` -* 路径:`/api/v1/playx/assets` +* 路径:`/api/v1/mall/assets` 请求参数(二选一): * `session_id`(优先):从 `mall_playx_session` 查 user_id(并校验过期) @@ -282,7 +282,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/verify-token' \ ##### 示例 ```bash -curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'session_id=7b1c....' +curl -G 'http://localhost:1818/api/v1/mall/assets' --data-urlencode 'session_id=7b1c....' ``` ```json @@ -301,7 +301,7 @@ curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'session_id #### 领取(Claim) * 方法:`POST` -* 路径:`/api/v1/playx/claim` +* 路径:`/api/v1/mall/claim` 请求: * `claim_request_id`:幂等键(string,必填且唯一) @@ -312,7 +312,7 @@ curl -G 'http://localhost:1818/api/v1/playx/assets' --data-urlencode 'session_id ##### 示例 (首次领取成功,可能返回 `msg=Claim success`;若幂等重复,`msg` 可能为空) ```bash -curl -X POST 'http://localhost:1818/api/v1/playx/claim' \ +curl -X POST 'http://localhost:1818/api/v1/mall/claim' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'claim_request_id=claim_001' \ --data-urlencode 'session_id=7b1c....' @@ -334,7 +334,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/claim' \ #### 商品列表 * 方法:`GET` -* 路径:`/api/v1/playx/items` +* 路径:`/api/v1/mall/items` 请求(可选): * `type=BONUS|PHYSICAL|WITHDRAW` @@ -344,7 +344,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/claim' \ ##### 示例 ```bash -curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=BONUS' +curl -G 'http://localhost:1818/api/v1/mall/items' --data-urlencode 'type=BONUS' ``` ```json @@ -370,7 +370,7 @@ curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=BONUS' #### 红利兑换(Bonus Redeem) * 方法:`POST` -* 路径:`/api/v1/playx/bonus/redeem` +* 路径:`/api/v1/mall/bonusRedeem` 请求: * `item_id`:商品 ID(BONUS) @@ -382,7 +382,7 @@ curl -G 'http://localhost:1818/api/v1/playx/items' --data-urlencode 'type=BONUS' ##### 示例 ```bash -curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \ +curl -X POST 'http://localhost:1818/api/v1/mall/bonusRedeem' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'item_id=123' \ --data-urlencode 'session_id=7b1c....' @@ -401,13 +401,11 @@ curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \ #### 实物兑换(Physical Redeem) * 方法:`POST` -* 路径:`/api/v1/playx/physical/redeem` +* 路径:`/api/v1/mall/physicalRedeem` 请求: * `item_id`:商品 ID(PHYSICAL) -* `receiver_name` -* `receiver_phone` -* `receiver_address` +* `address_id`:`mall_address.id`(当前用户资产下地址;订单写入 `mall_address_id` 与收货快照) * 鉴权:`session_id` 或 `user_id` 成功返回: @@ -416,12 +414,10 @@ curl -X POST 'http://localhost:1818/api/v1/playx/bonus/redeem' \ ##### 示例 ```bash -curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \ +curl -X POST 'http://localhost:1818/api/v1/mall/physicalRedeem' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'item_id=200' \ - --data-urlencode 'receiver_name=张三' \ - --data-urlencode 'receiver_phone=18800001111' \ - --data-urlencode 'receiver_address=北京市朝阳区XX路XX号' \ + --data-urlencode 'address_id=10' \ --data-urlencode 'session_id=7b1c....' ``` @@ -435,7 +431,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \ #### 提现申请(Withdraw Apply) * 方法:`POST` -* 路径:`/api/v1/playx/withdraw/apply` +* 路径:`/api/v1/mall/withdrawApply` 请求: * `item_id`:商品 ID(WITHDRAW) @@ -447,7 +443,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/physical/redeem' \ ##### 示例 ```bash -curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \ +curl -X POST 'http://localhost:1818/api/v1/mall/withdrawApply' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'item_id=321' \ --data-urlencode 'session_id=7b1c....' @@ -466,7 +462,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \ #### 订单列表 * 方法:`GET` -* 路径:`/api/v1/playx/orders` +* 路径:`/api/v1/mall/orders` 请求: * `session_id` 或 `user_id` @@ -476,7 +472,7 @@ curl -X POST 'http://localhost:1818/api/v1/playx/withdraw/apply' \ ##### 示例 ```bash -curl -G 'http://localhost:1818/api/v1/playx/orders' --data-urlencode 'session_id=7b1c....' +curl -G 'http://localhost:1818/api/v1/mall/orders' --data-urlencode 'session_id=7b1c....' ``` ```json @@ -502,7 +498,7 @@ curl -G 'http://localhost:1818/api/v1/playx/orders' --data-urlencode 'session_id ``` #### 同步额度 -当前代码未实现并未注册路由:`/api/v1/playx/sync-limit`。 +当前代码未实现并未注册路由:`/api/v1/mall/syncLimit`。 如需补齐,请在接口设计阶段新增对应实现与 PlayX API 对接。 --- @@ -607,29 +603,30 @@ curl -G 'http://localhost:1818/api/v1/playx/orders' --data-urlencode 'session_id **表名**:`mall_playx_order`(或统一改造 mall_pints_order / mall_redemption_order) -| 字段 | 类型 | 说明 | -|------|------|------| -| id | int | 主键 | -| user_id | varchar(64) | 用户 ID | -| type | enum | BONUS / PHYSICAL / WITHDRAW | -| status | enum | PENDING / COMPLETED / SHIPPED / REJECTED | -| mall_item_id | int | 商品 ID | -| points_cost | int | 消耗积分 | -| amount | decimal(15,2) | 现金面值(红利/提现) | -| multiplier | int | 流水倍数 | -| external_transaction_id | varchar(64) | 外部交易幂等键 | -| playx_transaction_id | varchar(64) | PlayX 流水号 | +| 字段 | 类型 | 说明 | +|------|------|----------------------------------------------------------------------| +| id | int | 主键 | +| user_id | varchar(64) | 用户 ID | +| type | enum | BONUS / PHYSICAL / WITHDRAW | +| status | enum | PENDING / COMPLETED / SHIPPED / REJECTED | +| mall_item_id | int | 商品 ID | +| points_cost | int | 消耗积分 | +| amount | decimal(15,2) | 现金面值(红利/提现) | +| multiplier | int | 流水倍数 | +| external_transaction_id | varchar(64) | 订单号 | +| playx_transaction_id | varchar(64) | PlayX 流水号 | | grant_status | enum | NOT_SENT / SENT_PENDING / ACCEPTED / FAILED_RETRYABLE / FAILED_FINAL | -| fail_reason | text | 失败原因 | -| retry_count | int | 重试次数 | -| reject_reason | varchar(255) | 驳回原因(实物) | -| shipping_company | varchar(50) | 物流公司 | -| shipping_no | varchar(64) | 物流单号 | -| receiver_name | varchar(50) | 收货人 | -| receiver_phone | varchar(20) | 收货电话 | -| receiver_address | text | 收货地址 | -| create_time | bigint | 创建时间 | -| update_time | bigint | 更新时间 | +| fail_reason | text | 失败原因 | +| retry_count | int | 重试次数 | +| reject_reason | varchar(255) | 驳回原因(实物) | +| shipping_company | varchar(50) | 物流公司 | +| shipping_no | varchar(64) | 物流单号 | +| mall_address_id | int unsigned, NULL | 实物兑换所选 `mall_address.id`(快照仍见 `receiver_*`) | +| receiver_name | varchar(50) | 收货人 | +| receiver_phone | varchar(20) | 收货电话 | +| receiver_address | text | 收货地址 | +| create_time | bigint | 创建时间 | +| update_time | bigint | 更新时间 | **索引**:`user_id`、`external_transaction_id`、`type`、`status` diff --git a/web/src/lang/backend/en/mall/address.ts b/web/src/lang/backend/en/mall/address.ts index d143ee4..e873228 100644 --- a/web/src/lang/backend/en/mall/address.ts +++ b/web/src/lang/backend/en/mall/address.ts @@ -2,10 +2,10 @@ export default { id: 'id', playx_user_asset_id: 'PlayX user asset', playxuserasset__username: 'username', + receiver_name: 'receiver name', phone: 'phone', region: 'region', detail_address: 'detail_address', - address: 'address', default_setting: 'Default address', 'default_setting 0': '--', 'default_setting 1': 'YES', diff --git a/web/src/lang/backend/en/mall/order.ts b/web/src/lang/backend/en/mall/order.ts index bc56d59..5661cab 100644 --- a/web/src/lang/backend/en/mall/order.ts +++ b/web/src/lang/backend/en/mall/order.ts @@ -30,6 +30,7 @@ export default { receiver_name: 'receiver_name', receiver_phone: 'receiver_phone', receiver_address: 'receiver_address', + mall_address_id: 'mall_address_id', create_time: 'create_time', update_time: 'update_time', 'quick Search Fields': 'ID', diff --git a/web/src/lang/backend/zh-cn/mall/address.ts b/web/src/lang/backend/zh-cn/mall/address.ts index bf0e4a2..9c07ede 100644 --- a/web/src/lang/backend/zh-cn/mall/address.ts +++ b/web/src/lang/backend/zh-cn/mall/address.ts @@ -2,10 +2,10 @@ export default { id: 'ID', playx_user_asset_id: '用户资产', playxuserasset__username: '用户名', + receiver_name: '收货人', phone: '电话', region: '地区', detail_address: '详细地址', - address: '地址', default_setting: '默认地址', 'default_setting 0': '--', 'default_setting 1': '是', diff --git a/web/src/lang/backend/zh-cn/mall/claimLog.ts b/web/src/lang/backend/zh-cn/mall/claimLog.ts index 1ef1f11..ce7981f 100644 --- a/web/src/lang/backend/zh-cn/mall/claimLog.ts +++ b/web/src/lang/backend/zh-cn/mall/claimLog.ts @@ -1,6 +1,6 @@ export default { id: 'ID', - claim_request_id: '领取幂等键', + claim_request_id: '领取订单号', user_id: '用户ID', claimed_amount: '领取积分', create_time: '创建时间', diff --git a/web/src/lang/backend/zh-cn/mall/order.ts b/web/src/lang/backend/zh-cn/mall/order.ts index 99ae053..c771e60 100644 --- a/web/src/lang/backend/zh-cn/mall/order.ts +++ b/web/src/lang/backend/zh-cn/mall/order.ts @@ -15,7 +15,7 @@ export default { points_cost: '消耗积分', amount: '现金面值', multiplier: '流水倍数', - external_transaction_id: '外部交易幂等键', + external_transaction_id: '订单号', playx_transaction_id: 'PlayX流水号', grant_status: '发放子状态', 'grant_status NOT_SENT': '未发送', @@ -30,6 +30,7 @@ export default { receiver_name: '收货人', receiver_phone: '收货电话', receiver_address: '收货地址', + mall_address_id: '地址ID', create_time: '创建时间', update_time: '修改时间', 'quick Search Fields': 'ID', diff --git a/web/src/lang/backend/zh-cn/mall/playxClaimLog.ts b/web/src/lang/backend/zh-cn/mall/playxClaimLog.ts index 1ef1f11..ce7981f 100644 --- a/web/src/lang/backend/zh-cn/mall/playxClaimLog.ts +++ b/web/src/lang/backend/zh-cn/mall/playxClaimLog.ts @@ -1,6 +1,6 @@ export default { id: 'ID', - claim_request_id: '领取幂等键', + claim_request_id: '领取订单号', user_id: '用户ID', claimed_amount: '领取积分', create_time: '创建时间', diff --git a/web/src/lang/backend/zh-cn/mall/playxOrder.ts b/web/src/lang/backend/zh-cn/mall/playxOrder.ts index 99ae053..b4bfef3 100644 --- a/web/src/lang/backend/zh-cn/mall/playxOrder.ts +++ b/web/src/lang/backend/zh-cn/mall/playxOrder.ts @@ -15,7 +15,7 @@ export default { points_cost: '消耗积分', amount: '现金面值', multiplier: '流水倍数', - external_transaction_id: '外部交易幂等键', + external_transaction_id: '订单号', playx_transaction_id: 'PlayX流水号', grant_status: '发放子状态', 'grant_status NOT_SENT': '未发送', diff --git a/web/src/views/backend/mall/address/index.vue b/web/src/views/backend/mall/address/index.vue index 80a5553..6cdddfd 100644 --- a/web/src/views/backend/mall/address/index.vue +++ b/web/src/views/backend/mall/address/index.vue @@ -37,6 +37,17 @@ const { t } = useI18n() const tableRef = useTemplateRef('tableRef') const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete']) +const hasChinese = (s: string) => /[\u4e00-\u9fa5]/.test(s) + +const formatRegion = (raw: string) => { + const s = raw.toString().trim() + if (!s) return '' + if (hasChinese(s)) { + return s.replace(/[,,\s]+/g, ',') + } + return s.replace(/[,,\s]+/g, ',') +} + /** * baTable 内包含了表格的所有数据且数据具备响应性,然后通过 provide 注入给了后代组件 */ @@ -53,10 +64,19 @@ const baTable = new baTableClass( align: 'center', minWidth: 120, operatorPlaceholder: t('Fuzzy query'), - render: 'tags', + showOverflowTooltip: true, operator: 'LIKE', comSearchRender: 'string', }, + { + label: t('mall.address.receiver_name'), + prop: 'receiver_name', + align: 'center', + minWidth: 100, + operatorPlaceholder: t('Fuzzy query'), + sortable: false, + operator: 'LIKE', + }, { label: t('mall.address.phone'), prop: 'phone', @@ -65,7 +85,17 @@ const baTable = new baTableClass( sortable: false, operator: 'LIKE', }, - { label: t('mall.address.region'), prop: 'region_text', align: 'center', operator: false }, + { + label: t('mall.address.region'), + prop: 'region_text', + align: 'center', + operator: false, + showOverflowTooltip: true, + formatter: (row: TableRow, _column: TableColumn, cellValue: string) => { + const raw = (cellValue || row.region || '').toString() + return formatRegion(raw) + }, + }, { label: t('mall.address.detail_address'), prop: 'detail_address', @@ -107,7 +137,7 @@ const baTable = new baTableClass( width: 160, timeFormat: 'yyyy-mm-dd hh:MM:ss', }, - { label: t('Operate'), align: 'center', width: 100, render: 'buttons', buttons: optButtons, operator: false }, + { label: t('Operate'), align: 'center', width: 80, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' }, ], dblClickNotEditColumn: [undefined, 'default_setting'], }, diff --git a/web/src/views/backend/mall/address/popupForm.vue b/web/src/views/backend/mall/address/popupForm.vue index 577e8c4..a27975b 100644 --- a/web/src/views/backend/mall/address/popupForm.vue +++ b/web/src/views/backend/mall/address/popupForm.vue @@ -37,6 +37,13 @@ :input-attr="{ pk: 'mall_user_asset.id', field: 'username', remoteUrl: '/admin/mall.UserAsset/select' }" :placeholder="t('Please select field', { field: t('mall.address.playx_user_asset_id') })" /> + - { - diff --git a/web/src/views/backend/mall/item/index.vue b/web/src/views/backend/mall/item/index.vue index fbca1a5..00c21b4 100644 --- a/web/src/views/backend/mall/item/index.vue +++ b/web/src/views/backend/mall/item/index.vue @@ -52,6 +52,8 @@ const baTable = new baTableClass( label: t('mall.item.description'), prop: 'description', align: 'center', + minWidth: 80, + // showOverflowTooltip: true, operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE', @@ -60,6 +62,7 @@ const baTable = new baTableClass( label: t('mall.item.score'), prop: 'score', align: 'center', + minWidth: 90, sortable: false, operator: 'RANGE', }, @@ -173,7 +176,7 @@ const baTable = new baTableClass( { label: t('Operate'), align: 'center', - width: 100, + width: 80, render: 'buttons', buttons: optButtons, operator: false, diff --git a/web/src/views/backend/mall/order/index.vue b/web/src/views/backend/mall/order/index.vue index 3da8d7a..8dcec94 100644 --- a/web/src/views/backend/mall/order/index.vue +++ b/web/src/views/backend/mall/order/index.vue @@ -3,11 +3,13 @@
+ + @@ -19,7 +21,8 @@ import createAxios from '/@/utils/axios' import TableHeader from '/@/components/table/header/index.vue' import Table from '/@/components/table/index.vue' import baTableClass from '/@/utils/baTable' -import { ElMessageBox } from 'element-plus' +import PopupForm from './popupForm.vue' +import { defaultOptButtons } from '/@/components/table' defineOptions({ name: 'mall/order', @@ -27,6 +30,17 @@ defineOptions({ const { t } = useI18n() const tableRef = useTemplateRef('tableRef') +const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete']).map((btn) => + btn.name === 'edit' + ? { + ...btn, + title: '审核', + type: 'primary', + class: 'table-row-edit', + icon: 'fa fa-check', + } + : btn +) const baTable = new baTableClass( new baTableApi('/admin/mall.Order/'), @@ -35,11 +49,21 @@ const baTable = new baTableClass( column: [ { type: 'selection', align: 'center', operator: false }, { label: t('mall.order.id'), prop: 'id', align: 'center', width: 70, operator: 'RANGE', sortable: 'custom' }, - { label: t('mall.order.user_id'), prop: 'user_id', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' }, + { + label: t('mall.order.user_id'), + prop: 'user_id', + align: 'center', + operatorPlaceholder: t('Fuzzy query'), + sortable: false, + operator: 'LIKE', + }, { label: t('mall.order.type'), prop: 'type', align: 'center', + effect: 'light', + custom: { BONUS: 'success', PHYSICAL: 'primary', WITHDRAW: 'info' }, + minWidth: 140, operator: 'eq', sortable: false, render: 'tag', @@ -53,6 +77,9 @@ const baTable = new baTableClass( label: t('mall.order.status'), prop: 'status', align: 'center', + effect: 'dark', + custom: { PENDING: 'success', COMPLETED: 'primary', SHIPPED: 'info', REJECTED: 'loading' }, + minWidth: 160, operator: 'eq', sortable: false, render: 'tag', @@ -64,12 +91,36 @@ const baTable = new baTableClass( }, }, { label: t('mall.order.mall_item_id'), prop: 'mall_item_id', align: 'center', operator: 'RANGE', sortable: false }, - { label: t('mall.order.mallitem__title'), prop: 'mallItem.title', align: 'center', operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' }, - { label: t('mall.order.points_cost'), prop: 'points_cost', align: 'center', operator: 'RANGE', sortable: false }, - { label: t('mall.order.amount'), prop: 'amount', align: 'center', operator: 'RANGE', sortable: false }, - { label: t('mall.order.multiplier'), prop: 'multiplier', align: 'center', operator: 'eq', sortable: false }, - { label: t('mall.order.external_transaction_id'), prop: 'external_transaction_id', align: 'center', operatorPlaceholder: t('Fuzzy query'), 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.mallitem__title'), + prop: 'mallItem.title', + align: 'center', + minWidth: 90, + operatorPlaceholder: t('Fuzzy query'), + sortable: false, + operator: 'LIKE', + }, + { label: t('mall.order.points_cost'), prop: 'points_cost', align: 'center', minWidth: 90, operator: 'RANGE', sortable: false }, + { label: t('mall.order.amount'), prop: 'amount', align: 'center', minWidth: 90, operator: 'RANGE', sortable: false }, + { label: t('mall.order.multiplier'), prop: 'multiplier', align: 'center', minWidth: 90, operator: 'eq', sortable: false }, + { + label: t('mall.order.external_transaction_id'), + prop: 'external_transaction_id', + align: 'center', + minWidth: 80, + showOverflowTooltip: true, + operatorPlaceholder: t('Fuzzy query'), + 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', @@ -85,21 +136,100 @@ const baTable = new baTableClass( FAILED_FINAL: t('mall.order.grant_status FAILED_FINAL'), }, }, - { label: t('mall.order.fail_reason'), prop: 'fail_reason', align: 'center', showOverflowTooltip: true, operatorPlaceholder: t('Fuzzy query'), sortable: false, operator: 'LIKE' }, - { label: t('mall.order.reject_reason'), prop: 'reject_reason', align: 'center', showOverflowTooltip: true, sortable: false, operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') }, - { label: t('mall.order.shipping_company'), prop: 'shipping_company', align: 'center', showOverflowTooltip: true, sortable: false, operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') }, - { label: t('mall.order.shipping_no'), prop: 'shipping_no', align: 'center', showOverflowTooltip: true, sortable: false, operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') }, - { label: t('mall.order.receiver_name'), prop: 'receiver_name', align: 'center', showOverflowTooltip: true, sortable: false, operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') }, - { label: t('mall.order.receiver_phone'), prop: 'receiver_phone', align: 'center', showOverflowTooltip: true, sortable: false, operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') }, - { label: t('mall.order.receiver_address'), prop: 'receiver_address', align: 'center', showOverflowTooltip: true, sortable: false, operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') }, - { label: t('mall.order.create_time'), prop: 'create_time', align: 'center', render: 'datetime', operator: 'RANGE', comSearchRender: 'datetime', sortable: 'custom', width: 160, timeFormat: 'yyyy-mm-dd hh:MM:ss' }, - { label: t('mall.order.update_time'), prop: 'update_time', align: 'center', render: 'datetime', operator: 'RANGE', comSearchRender: 'datetime', sortable: 'custom', width: 160, timeFormat: 'yyyy-mm-dd hh:MM:ss' }, + { + label: t('mall.order.fail_reason'), + prop: 'fail_reason', + align: 'center', + showOverflowTooltip: true, + operatorPlaceholder: t('Fuzzy query'), + sortable: false, + operator: 'LIKE', + }, + { + label: t('mall.order.reject_reason'), + prop: 'reject_reason', + align: 'center', + showOverflowTooltip: true, + sortable: false, + operator: 'LIKE', + operatorPlaceholder: t('Fuzzy query'), + }, + { + label: t('mall.order.shipping_company'), + prop: 'shipping_company', + align: 'center', + showOverflowTooltip: true, + sortable: false, + operator: 'LIKE', + operatorPlaceholder: t('Fuzzy query'), + }, + { + label: t('mall.order.shipping_no'), + prop: 'shipping_no', + align: 'center', + showOverflowTooltip: true, + sortable: false, + operator: 'LIKE', + operatorPlaceholder: t('Fuzzy query'), + }, + { + label: t('mall.order.receiver_name'), + prop: 'receiver_name', + align: 'center', + showOverflowTooltip: true, + sortable: false, + operator: 'LIKE', + operatorPlaceholder: t('Fuzzy query'), + }, + { + label: t('mall.order.receiver_phone'), + prop: 'receiver_phone', + align: 'center', + showOverflowTooltip: true, + sortable: false, + operator: 'LIKE', + operatorPlaceholder: t('Fuzzy query'), + }, + { + label: t('mall.order.receiver_address'), + prop: 'receiver_address', + align: 'center', + showOverflowTooltip: true, + sortable: false, + operator: 'LIKE', + operatorPlaceholder: t('Fuzzy query'), + }, + { label: t('mall.order.mall_address_id'), prop: 'mall_address_id', align: 'center', width: 100, operator: 'eq', sortable: false }, + { + label: t('mall.order.create_time'), + prop: 'create_time', + align: 'center', + render: 'datetime', + operator: 'RANGE', + comSearchRender: 'datetime', + sortable: 'custom', + width: 160, + timeFormat: 'yyyy-mm-dd hh:MM:ss', + }, + { + label: t('mall.order.update_time'), + prop: 'update_time', + align: 'center', + render: 'datetime', + operator: 'RANGE', + comSearchRender: 'datetime', + sortable: 'custom', + width: 160, + timeFormat: 'yyyy-mm-dd hh:MM:ss', + }, { label: t('Operate'), align: 'center', - width: 220, + width: 80, + fixed: 'right', render: 'buttons', buttons: [ + ...optButtons, { render: 'confirmButton', name: 'retry', @@ -131,81 +261,6 @@ const baTable = new baTableClass( await baTable.getData() }, }, - { - render: 'basicButton', - name: 'ship', - title: 'Ship', - text: '发货', - type: 'success', - icon: '', - display: (row: TableRow) => row.type === 'PHYSICAL' && row.status === 'PENDING', - click: async (row: TableRow) => { - try { - const shippingNoRes = await ElMessageBox.prompt('请输入物流单号', '发货', { - confirmButtonText: '确认', - cancelButtonText: '取消', - }) - const shippingCompanyRes = await ElMessageBox.prompt('请输入物流公司', '发货', { - confirmButtonText: '确认', - cancelButtonText: '取消', - }) - - const shippingNo = shippingNoRes.value - const shippingCompany = shippingCompanyRes.value - - await createAxios( - { - url: '/admin/mall.Order/ship', - method: 'post', - data: { - id: row.id, - shipping_company: shippingCompany, - shipping_no: shippingNo, - }, - }, - { - showSuccessMessage: true, - } - ) - await baTable.getData() - } catch { - // 用户取消弹窗:不做任何提示,避免控制台报错 - } - }, - }, - { - render: 'basicButton', - name: 'reject', - title: 'Reject', - text: '驳回', - type: 'danger', - icon: '', - display: (row: TableRow) => row.type === 'PHYSICAL' && row.status === 'PENDING', - click: async (row: TableRow) => { - try { - const res = await ElMessageBox.prompt('请输入驳回原因', '驳回', { - confirmButtonText: '确认', - cancelButtonText: '取消', - }) - await createAxios( - { - url: '/admin/mall.Order/reject', - method: 'post', - data: { - id: row.id, - reject_reason: res.value, - }, - }, - { - showSuccessMessage: true, - } - ) - await baTable.getData() - } catch { - // 用户取消:不提示 - } - }, - }, ], }, ], @@ -229,4 +284,3 @@ onMounted(() => { - diff --git a/web/src/views/backend/mall/order/popupForm.vue b/web/src/views/backend/mall/order/popupForm.vue new file mode 100644 index 0000000..1d09d49 --- /dev/null +++ b/web/src/views/backend/mall/order/popupForm.vue @@ -0,0 +1,290 @@ + + + + + + diff --git a/web/src/views/backend/mall/userAsset/index.vue b/web/src/views/backend/mall/userAsset/index.vue index ee3aec1..621e756 100644 --- a/web/src/views/backend/mall/userAsset/index.vue +++ b/web/src/views/backend/mall/userAsset/index.vue @@ -3,18 +3,22 @@
+ + - diff --git a/web/src/views/backend/mall/userAsset/popupForm.vue b/web/src/views/backend/mall/userAsset/popupForm.vue new file mode 100644 index 0000000..63c4b42 --- /dev/null +++ b/web/src/views/backend/mall/userAsset/popupForm.vue @@ -0,0 +1,124 @@ + + + + + +