Compare commits

...

8 Commits

27 changed files with 620 additions and 256 deletions

View File

@@ -66,7 +66,7 @@ class Dashboard extends Backend
$pendingPhysicalToShip = MallOrder::where('type', MallOrder::TYPE_PHYSICAL) $pendingPhysicalToShip = MallOrder::where('type', MallOrder::TYPE_PHYSICAL)
->where('status', MallOrder::STATUS_PENDING) ->where('status', MallOrder::STATUS_PENDING)
->count(); ->count();
$grantFailedRetryableCount = MallOrder::whereIn('type', [MallOrder::TYPE_BONUS, MallOrder::TYPE_WITHDRAW]) $grantFailedRetryableCount = MallOrder::where('type', MallOrder::TYPE_BONUS)
->where('grant_status', MallOrder::GRANT_FAILED_RETRYABLE) ->where('grant_status', MallOrder::GRANT_FAILED_RETRYABLE)
->count(); ->count();

View File

@@ -4,6 +4,7 @@ namespace app\admin\controller\mall;
use Throwable; use Throwable;
use app\common\controller\Backend; use app\common\controller\Backend;
use app\common\library\MallBonusGrantPush;
use app\common\model\MallOrder; use app\common\model\MallOrder;
use app\common\model\MallUserAsset; use app\common\model\MallUserAsset;
use support\think\Db; use support\think\Db;
@@ -202,7 +203,7 @@ class Order extends Backend
$id = $data['id'] ?? 0; $id = $data['id'] ?? 0;
$rejectReason = $data['reject_reason'] ?? ''; $rejectReason = $data['reject_reason'] ?? '';
if (!$id || $rejectReason === '') { if (!$id) {
return $this->error(__('Missing required fields')); return $this->error(__('Missing required fields'));
} }
@@ -214,6 +215,10 @@ class Order extends Backend
return $this->error(__('Order status must be PENDING')); return $this->error(__('Order status must be PENDING'));
} }
if ($order->type === MallOrder::TYPE_PHYSICAL && $rejectReason === '') {
return $this->error(__('Missing required fields'));
}
Db::startTrans(); Db::startTrans();
try { try {
$asset = MallUserAsset::where('playx_user_id', $order->user_id ?? '')->find(); $asset = MallUserAsset::where('playx_user_id', $order->user_id ?? '')->find();
@@ -229,7 +234,11 @@ class Order extends Backend
$order->status = MallOrder::STATUS_REJECTED; $order->status = MallOrder::STATUS_REJECTED;
$order->reject_reason = $rejectReason; $order->reject_reason = $rejectReason;
$order->grant_status = MallOrder::GRANT_FAILED_FINAL; if ($order->type === MallOrder::TYPE_BONUS) {
$order->grant_status = MallOrder::GRANT_FAILED_FINAL;
} else {
$order->grant_status = MallOrder::GRANT_NOT_APPLICABLE;
}
$order->update_time = time(); $order->update_time = time();
$order->save(); $order->save();
@@ -243,7 +252,7 @@ class Order extends Backend
} }
/** /**
* 手动重试(仅红利/提现,且必须 FAILED_RETRYABLE * 手动推送红利(同步调用 PlayX不限制自动重试次数成功则 ACCEPTED失败写入 fail_reason
*/ */
public function retry(Request $request): Response public function retry(Request $request): Response
{ {
@@ -265,20 +274,45 @@ class Order extends Backend
if (!$order) { if (!$order) {
return $this->error(__('Record not found')); return $this->error(__('Record not found'));
} }
if (!in_array($order->type, [MallOrder::TYPE_BONUS, MallOrder::TYPE_WITHDRAW], true)) { if ($order->type !== MallOrder::TYPE_BONUS) {
return $this->error(__('Only BONUS/WITHDRAW can retry')); return $this->error(__('Only BONUS can retry'));
} }
if ($order->grant_status !== MallOrder::GRANT_FAILED_RETRYABLE) { if ($order->status !== MallOrder::STATUS_PENDING) {
return $this->error(__('Only FAILED_RETRYABLE can retry')); return $this->error(__('Order status must be PENDING'));
}
if (($order->retry_count ?? 0) >= 3) {
return $this->error(__('Retry count exceeded'));
} }
$order->grant_status = MallOrder::GRANT_NOT_SENT; $allowedStatuses = [
MallOrder::GRANT_NOT_SENT,
MallOrder::GRANT_SENT_PENDING,
MallOrder::GRANT_FAILED_RETRYABLE,
MallOrder::GRANT_FAILED_FINAL,
];
if (!in_array($order->grant_status, $allowedStatuses, true)) {
return $this->error(__('Current grant status cannot be manually pushed'));
}
if (strval(config('playx.api.base_url', '')) === '') {
return $this->error(__('PlayX API not configured'));
}
$result = MallBonusGrantPush::push($order);
if ($result['ok']) {
$order->grant_status = MallOrder::GRANT_ACCEPTED;
$order->playx_transaction_id = $result['playx_transaction_id'];
$order->fail_reason = null;
$order->update_time = time();
$order->save();
return $this->success(__('Push succeeded'));
}
$failReason = __('Manual push failed') . ': ' . $result['message'];
$order->fail_reason = $failReason;
$order->grant_status = MallOrder::GRANT_FAILED_FINAL;
$order->update_time = time();
$order->save(); $order->save();
return $this->success(__('Retry queued')); return $this->error($failReason);
} }
} }

View File

@@ -95,4 +95,9 @@ return [
'%d records and files have been deleted' => '%d records and files have been deleted', '%d records and files have been deleted' => '%d records and files have been deleted',
'Please input correct username' => 'Please enter the correct username', 'Please input correct username' => 'Please enter the correct username',
'Group Name Arr' => 'Group Name Arr', 'Group Name Arr' => 'Group Name Arr',
'Push succeeded' => 'Push succeeded',
'Manual push failed' => 'Manual push failed',
'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',
]; ];

View File

@@ -114,4 +114,9 @@ return [
'%d records and files have been deleted' => '已删除%d条记录和文件', '%d records and files have been deleted' => '已删除%d条记录和文件',
'Please input correct username' => '请输入正确的用户名', 'Please input correct username' => '请输入正确的用户名',
'Group Name Arr' => '分组名称数组', 'Group Name Arr' => '分组名称数组',
'Push succeeded' => '推送成功',
'Manual push failed' => '手动推送失败',
'PlayX API not configured' => 'PlayX 接口未配置',
'Current grant status cannot be manually pushed' => '当前发放状态不可手动推送',
'Order status must be PENDING' => '订单状态须为处理中',
]; ];

View File

@@ -148,7 +148,7 @@ class Playx extends Api
/** /**
* Daily Push API - PlayX 调用商城接收 T+1 数据 * Daily Push API - PlayX 调用商城接收 T+1 数据
* POST /api/v1/playx/daily-push * POST /api/v1/mall/dailyPush
*/ */
public function dailyPush(Request $request): Response public function dailyPush(Request $request): Response
{ {
@@ -173,7 +173,7 @@ class Playx extends Api
if ($sig === '' || $ts === '' || $rid === '') { if ($sig === '' || $ts === '' || $rid === '') {
return $this->error(__('Daily push signature missing or incomplete'), null, 0, ['statusCode' => 401]); return $this->error(__('Daily push signature missing or incomplete'), null, 0, ['statusCode' => 401]);
} }
$canonical = $ts . "\n" . $rid . "\nPOST\n/api/v1/playx/daily-push\n" . hash('sha256', json_encode($body)); $canonical = $ts . "\n" . $rid . "\nPOST\n/api/v1/mall/dailyPush\n" . hash('sha256', json_encode($body));
$expected = hash_hmac('sha256', $canonical, $secret); $expected = hash_hmac('sha256', $canonical, $secret);
if (!hash_equals($expected, $sig)) { if (!hash_equals($expected, $sig)) {
return $this->error(__('Daily push signature verification failed'), null, 0, ['statusCode' => 401]); return $this->error(__('Daily push signature verification failed'), null, 0, ['statusCode' => 401]);
@@ -942,6 +942,8 @@ class Playx extends Api
'grant_status' => MallOrder::GRANT_NOT_SENT, 'grant_status' => MallOrder::GRANT_NOT_SENT,
'create_time' => time(), 'create_time' => time(),
'update_time' => time(), 'update_time' => time(),
'start_time' => date('Y-m-d H:i:s', time()),
'end_time' => date('Y-m-d H:i:s', time()+86400*3),
]); ]);
Db::commit(); Db::commit();
@@ -1016,6 +1018,7 @@ class Playx extends Api
'receiver_name' => $snapshot['receiver_name'], 'receiver_name' => $snapshot['receiver_name'],
'receiver_phone' => $snapshot['receiver_phone'], 'receiver_phone' => $snapshot['receiver_phone'],
'receiver_address' => $snapshot['receiver_address'], 'receiver_address' => $snapshot['receiver_address'],
'grant_status' => MallOrder::GRANT_NOT_APPLICABLE,
'create_time' => time(), 'create_time' => time(),
'update_time' => time(), 'update_time' => time(),
]); ]);
@@ -1082,7 +1085,7 @@ class Playx extends Api
'amount' => $amount, 'amount' => $amount,
'multiplier' => $multiplier, 'multiplier' => $multiplier,
'external_transaction_id' => $orderNo, 'external_transaction_id' => $orderNo,
'grant_status' => MallOrder::GRANT_NOT_SENT, 'grant_status' => MallOrder::GRANT_NOT_APPLICABLE,
'create_time' => time(), 'create_time' => time(),
'update_time' => time(), 'update_time' => time(),
]); ]);
@@ -1093,11 +1096,6 @@ class Playx extends Api
return $this->error($e->getMessage()); return $this->error($e->getMessage());
} }
$baseUrl = config('playx.api.base_url', '');
if ($baseUrl !== '') {
$this->callPlayxBalanceCredit($order, $playxUserId);
}
return $this->success(__('Withdraw submitted, please wait about 10 minutes'), [ return $this->success(__('Withdraw submitted, please wait about 10 minutes'), [
'order_id' => $order->id, 'order_id' => $order->id,
'status' => 'PENDING', 'status' => 'PENDING',
@@ -1143,39 +1141,4 @@ class Playx extends Api
} }
} }
private function callPlayxBalanceCredit(MallOrder $order, string $userId): void
{
$baseUrl = rtrim(config('playx.api.base_url', ''), '/');
$url = config('playx.api.balance_credit_url', '/api/v1/balance/credit');
if ($baseUrl === '') {
return;
}
try {
$client = new \GuzzleHttp\Client(['timeout' => 15]);
$res = $client->post($baseUrl . $url, [
'json' => [
'request_id' => 'mall_withdraw_' . uniqid(),
'externalTransactionId' => $order->external_transaction_id,
'user_id' => $userId,
'amount' => $order->amount,
'multiplier' => $order->multiplier,
],
]);
$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 {
$order->grant_status = MallOrder::GRANT_FAILED_RETRYABLE;
$order->fail_reason = $data['message'] ?? 'unknown';
$order->save();
}
} catch (\Throwable $e) {
$order->grant_status = MallOrder::GRANT_FAILED_RETRYABLE;
$order->fail_reason = $e->getMessage();
$order->save();
}
}
} }

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace app\common\library;
use app\common\model\MallItem;
use app\common\model\MallOrder;
use GuzzleHttp\Client;
/**
* 红利订单调用 PlayX bonus/grant与定时任务、后台手动推送共用
*/
final class MallBonusGrantPush
{
/**
* @return array{ok: bool, message: string, playx_transaction_id: string}
*/
public static function push(MallOrder $order): array
{
$baseUrl = rtrim(strval(config('playx.api.base_url', '')), '/');
if ($baseUrl === '') {
return [
'ok' => false,
'message' => 'PlayX base_url not configured',
'playx_transaction_id' => '',
];
}
$path = strval(config('playx.api.bonus_grant_url', '/api/v1/bonus/grant'));
$url = $baseUrl . $path;
$item = MallItem::where('id', $order->mall_item_id)->find();
$rewardName = $item ? strval($item->title) : '';
$category = $item ? strval($item->category) : 'daily';
$categoryTitle = $item ? strval($item->category_title) : '';
$multiplier = intval($order->multiplier ?? 0);
if ($multiplier <= 0) {
$multiplier = 1;
}
$client = new Client([
'timeout' => 20,
'http_errors' => false,
]);
$requestId = 'mall_bonus_' . uniqid();
try {
$res = $client->post($url, [
'json' => [
'request_id' => $requestId,
'externalTransactionId' => $order->external_transaction_id,
'user_id' => $order->user_id,
'amount' => $order->amount,
'rewardName' => $rewardName,
'category' => $category,
'categoryTitle' => $categoryTitle,
'multiplier' => $multiplier,
],
]);
$data = json_decode(strval($res->getBody()), true) ?? [];
if ($res->getStatusCode() === 200 && ($data['status'] ?? '') === 'accepted') {
return [
'ok' => true,
'message' => '',
'playx_transaction_id' => strval($data['playx_transaction_id'] ?? ''),
];
}
return [
'ok' => false,
'message' => strval($data['message'] ?? 'PlayX bonus grant not accepted'),
'playx_transaction_id' => '',
];
} catch (\Throwable $e) {
return [
'ok' => false,
'message' => $e->getMessage(),
'playx_transaction_id' => '',
];
}
}
}

View File

@@ -51,6 +51,9 @@ class MallOrder extends Model
public const GRANT_FAILED_RETRYABLE = 'FAILED_RETRYABLE'; public const GRANT_FAILED_RETRYABLE = 'FAILED_RETRYABLE';
public const GRANT_FAILED_FINAL = 'FAILED_FINAL'; public const GRANT_FAILED_FINAL = 'FAILED_FINAL';
/** 非红利订单不参与 PlayX/Angpow 推送,固定为该占位值 */
public const GRANT_NOT_APPLICABLE = '---';
protected array $type = [ protected array $type = [
'create_time' => 'integer', 'create_time' => 'integer',
'update_time' => 'integer', 'update_time' => 'integer',

View File

@@ -238,17 +238,17 @@ class AngpowImportJobs
return null; return null;
} }
$createTime = $order->create_time ?? null; // $createTime = $order->create_time ?? null;
if (!is_int($createTime)) { // if (!is_int($createTime)) {
if (is_numeric($createTime)) { // if (is_numeric($createTime)) {
$createTime = intval($createTime); // $createTime = intval($createTime);
} else { // } else {
$createTime = time(); // $createTime = time();
} // }
} // }
$start = gmdate('Y-m-d\TH:i:s\Z', $createTime); $start = gmdate('Y-m-d H:i:s', strtotime($order->start_time));
$end = gmdate('Y-m-d\TH:i:s\Z', $createTime + 86400); $end = gmdate('Y-m-d H:i:s', strtotime($order->end_time));
return [ return [
'member_login' => strval($asset->playx_user_id), 'member_login' => strval($asset->playx_user_id),

View File

@@ -2,7 +2,7 @@
namespace app\process; namespace app\process;
use app\common\model\MallItem; use app\common\library\MallBonusGrantPush;
use app\common\model\MallOrder; use app\common\model\MallOrder;
use app\common\model\MallUserAsset; use app\common\model\MallUserAsset;
use GuzzleHttp\Client; use GuzzleHttp\Client;
@@ -47,7 +47,8 @@ class PlayxJobs
$path = strval(config('playx.api.transaction_status_url', '/api/v1/transaction/status')); $path = strval(config('playx.api.transaction_status_url', '/api/v1/transaction/status'));
$url = rtrim($baseUrl, '/') . $path; $url = rtrim($baseUrl, '/') . $path;
$list = MallOrder::where('grant_status', MallOrder::GRANT_ACCEPTED) $list = MallOrder::where('type', MallOrder::TYPE_BONUS)
->where('grant_status', MallOrder::GRANT_ACCEPTED)
->where('status', MallOrder::STATUS_PENDING) ->where('status', MallOrder::STATUS_PENDING)
->order('id', 'desc') ->order('id', 'desc')
->limit(50) ->limit(50)
@@ -98,13 +99,9 @@ class PlayxJobs
return; return;
} }
$bonusPath = strval(config('playx.api.bonus_grant_url', '/api/v1/bonus/grant'));
$withdrawPath = strval(config('playx.api.balance_credit_url', '/api/v1/balance/credit'));
$bonusUrl = rtrim($baseUrl, '/') . $bonusPath;
$withdrawUrl = rtrim($baseUrl, '/') . $withdrawPath;
$maxRetry = 3; $maxRetry = 3;
$list = MallOrder::whereIn('grant_status', [ $list = MallOrder::where('type', MallOrder::TYPE_BONUS)
->whereIn('grant_status', [
MallOrder::GRANT_NOT_SENT, MallOrder::GRANT_NOT_SENT,
MallOrder::GRANT_FAILED_RETRYABLE, MallOrder::GRANT_FAILED_RETRYABLE,
]) ])
@@ -124,14 +121,12 @@ class PlayxJobs
$order->retry_count = intval($order->retry_count ?? 0) + 1; $order->retry_count = intval($order->retry_count ?? 0) + 1;
try { try {
$this->sendGrantByOrder($order, $bonusUrl, $withdrawUrl, $maxRetry); $this->sendGrantByOrder($order, $maxRetry);
} catch (\Throwable $e) { } catch (\Throwable $e) {
$order->fail_reason = $e->getMessage(); $order->fail_reason = $e->getMessage();
if (intval($order->retry_count) >= $maxRetry) { if (intval($order->retry_count) >= $maxRetry) {
$order->grant_status = MallOrder::GRANT_FAILED_FINAL; $order->grant_status = MallOrder::GRANT_FAILED_FINAL;
$order->status = MallOrder::STATUS_REJECTED;
$order->save(); $order->save();
$this->refundPoints($order);
} else { } else {
$order->grant_status = MallOrder::GRANT_FAILED_RETRYABLE; $order->grant_status = MallOrder::GRANT_FAILED_RETRYABLE;
$order->save(); $order->save();
@@ -167,99 +162,31 @@ class PlayxJobs
return false; return false;
} }
private function sendGrantByOrder(MallOrder $order, string $bonusUrl, string $withdrawUrl, int $maxRetry): void private function sendGrantByOrder(MallOrder $order, int $maxRetry): void
{ {
$item = null; if ($order->type !== MallOrder::TYPE_BONUS) {
if ($order->mallItem) {
$item = $order->mallItem;
} else {
$item = MallItem::where('id', $order->mall_item_id)->find();
}
if ($order->type === MallOrder::TYPE_BONUS) {
$rewardName = $item ? strval($item->title) : '';
$category = $item ? strval($item->category) : 'daily';
$categoryTitle = $item ? strval($item->category_title) : '';
$multiplier = intval($order->multiplier ?? 0);
if ($multiplier <= 0) {
$multiplier = 1;
}
$requestId = 'mall_retry_bonus_' . uniqid();
$res = $this->http->post($bonusUrl, [
'json' => [
'request_id' => $requestId,
'externalTransactionId' => $order->external_transaction_id,
'user_id' => $order->user_id,
'amount' => $order->amount,
'rewardName' => $rewardName,
'category' => $category,
'categoryTitle' => $categoryTitle,
'multiplier' => $multiplier,
],
]);
$data = json_decode(strval($res->getBody()), true) ?? [];
if ($res->getStatusCode() === 200 && ($data['status'] ?? '') === 'accepted') {
$order->grant_status = MallOrder::GRANT_ACCEPTED;
$order->playx_transaction_id = strval($data['playx_transaction_id'] ?? '');
$order->save();
return;
}
$order->fail_reason = strval($data['message'] ?? 'PlayX bonus grant not accepted');
if (intval($order->retry_count) >= $maxRetry) {
$order->grant_status = MallOrder::GRANT_FAILED_FINAL;
$order->status = MallOrder::STATUS_REJECTED;
$order->save();
$this->refundPoints($order);
return;
}
$order->grant_status = MallOrder::GRANT_FAILED_RETRYABLE;
$order->save();
return; return;
} }
if ($order->type === MallOrder::TYPE_WITHDRAW) { $result = MallBonusGrantPush::push($order);
$multiplier = intval($order->multiplier ?? 0); if ($result['ok']) {
if ($multiplier <= 0) { $order->grant_status = MallOrder::GRANT_ACCEPTED;
$multiplier = 1; $order->playx_transaction_id = $result['playx_transaction_id'];
}
$requestId = 'mall_retry_withdraw_' . uniqid();
$res = $this->http->post($withdrawUrl, [
'json' => [
'request_id' => $requestId,
'externalTransactionId' => $order->external_transaction_id,
'user_id' => $order->user_id,
'amount' => $order->amount,
'multiplier' => $multiplier,
],
]);
$data = json_decode(strval($res->getBody()), true) ?? [];
if ($res->getStatusCode() === 200 && ($data['status'] ?? '') === 'accepted') {
$order->grant_status = MallOrder::GRANT_ACCEPTED;
$order->playx_transaction_id = strval($data['playx_transaction_id'] ?? '');
$order->save();
return;
}
$order->fail_reason = strval($data['message'] ?? 'PlayX balance credit not accepted');
if (intval($order->retry_count) >= $maxRetry) {
$order->grant_status = MallOrder::GRANT_FAILED_FINAL;
$order->status = MallOrder::STATUS_REJECTED;
$order->save();
$this->refundPoints($order);
return;
}
$order->grant_status = MallOrder::GRANT_FAILED_RETRYABLE;
$order->save(); $order->save();
return; return;
} }
// PHYSICAL 目前由后台手工发货/驳回,不参与 PlayX 发放重试 $order->fail_reason = $result['message'];
if (intval($order->retry_count) >= $maxRetry) {
$order->grant_status = MallOrder::GRANT_FAILED_FINAL;
$order->save();
return;
}
$order->grant_status = MallOrder::GRANT_FAILED_RETRYABLE;
$order->save();
} }
private function refundPoints(MallOrder $order): void private function refundPoints(MallOrder $order): void

View File

@@ -13,7 +13,7 @@ return [
// Daily Push 签名校验PlayX 调用商城时使用) // Daily Push 签名校验PlayX 调用商城时使用)
'daily_push_secret' => strval(env('PLAYX_DAILY_PUSH_SECRET', '')), 'daily_push_secret' => strval(env('PLAYX_DAILY_PUSH_SECRET', '')),
/** /**
* 合作方 JWT 验签密钥HS256。非空时daily-push 等回调需带 Authorization: Bearer * 合作方 JWT 验签密钥HS256。非空时dailyPush 等回调需带 Authorization: Bearer
* 仅写入部署环境变量,勿提交仓库。 * 仅写入部署环境变量,勿提交仓库。
*/ */
'partner_jwt_secret' => strval(env('PLAYX_PARTNER_JWT_SECRET', '')), 'partner_jwt_secret' => strval(env('PLAYX_PARTNER_JWT_SECRET', '')),

View File

@@ -526,7 +526,7 @@ curl -G 'http://localhost:1818/api/v1/mall/orders' --data-urlencode 'session_id=
- 发货:录入物流公司、单号 → `SHIPPED` - 发货:录入物流公司、单号 → `SHIPPED`
- 驳回:录入驳回原因 → `REJECTED`,自动退回积分 - 驳回:录入驳回原因 → `REJECTED`,自动退回积分
- **红利/提现订单** - **红利/提现订单**
- 展示 `external_transaction_id``playx_transaction_id`发放子状态 - 展示 `external_transaction_id``playx_transaction_id`推送playx
- 手动重试:仅对 `FAILED_RETRYABLE` 状态,记录 `retry_request_id`、操作者、原因 - 手动重试:仅对 `FAILED_RETRYABLE` 状态,记录 `retry_request_id`、操作者、原因
### 2.3 用户资产与人工调账 ### 2.3 用户资产与人工调账

View File

@@ -1,13 +1,14 @@
<?php <?php
//脚本执行指令 php tmp_sig.php
$secret = '5590a339502b133f4d0c545c3cdad159a4827dfccb3f51bb110c56f9b96568ca'; $secret = '5590a339502b133f4d0c545c3cdad159a4827dfccb3f51bb110c56f9b96568ca';
$ts = '1700000123'; $ts = '1775525663';
$rid = 'req_1700000000_234567'; $rid = 'req_1775525663_123';
$body = [ $body = [
'report_date' => '1700000123', 'report_date' => '1775525663',
'member' => [ 'member' => [
[ [
'member_id' => '234567', 'member_id' => '123',
'login' => 'zhenhui', 'login' => 'zhenhui',
'ltv_deposit' => 1500, 'ltv_deposit' => 1500,
'ltv_withdrawal' => 1800, 'ltv_withdrawal' => 1800,
@@ -18,7 +19,7 @@ $body = [
]; ];
$json = json_encode($body); $json = json_encode($body);
$canonical = $ts . "\n" . $rid . "\nPOST\n/api/v1/playx/daily-push\n" . hash('sha256', $json); $canonical = $ts . "\n" . $rid . "\nPOST\n/api/v1/mall/dailyPush\n" . hash('sha256', $json);
echo "json={$json}\n"; echo "json={$json}\n";
echo "sha256=" . hash('sha256', $json) . "\n"; echo "sha256=" . hash('sha256', $json) . "\n";

View File

@@ -1,7 +1,10 @@
/** /**
* backend common language package * backend common language package
*/ */
import menu from './en/menu'
export default { export default {
menu,
Balance: 'Balance', Balance: 'Balance',
Integral: 'Integral', Integral: 'Integral',
Connection: 'connection', Connection: 'connection',

View File

@@ -1,38 +1,43 @@
export default { export default {
id: 'id', approve: 'Review',
user_id: 'user_id', manual_retry: 'Retry grant',
type: 'type', retry_confirm: 'Queue this order for grant retry?',
'type BONUS': 'Bonus(BONUS)', id: 'Order ID',
'type PHYSICAL': 'Physical(PHYSICAL)', user_id: 'User ID',
'type WITHDRAW': 'Withdraw(WITHDRAW)', type: 'Type',
status: 'status', 'type BONUS': 'Bonus',
'status PENDING': 'Pending(PENDING)', 'type PHYSICAL': 'Physical',
'status COMPLETED': 'Completed(COMPLETED)', 'type WITHDRAW': 'Withdraw',
'status SHIPPED': 'Shipped(SHIPPED)', status: 'Status',
'status REJECTED': 'Rejected(REJECTED)', 'status PENDING': 'Pending',
mall_item_id: 'mall_item_id', 'status COMPLETED': 'Completed',
mallitem__title: 'title', 'status SHIPPED': 'Shipped',
points_cost: 'points_cost', 'status REJECTED': 'Rejected',
amount: 'amount', mall_item_id: 'Product ID',
multiplier: 'multiplier', mallitem__title: 'Product title',
external_transaction_id: 'external_transaction_id', points_cost: 'Points spent',
playx_transaction_id: 'playx_transaction_id', amount: 'Cash amount',
grant_status: 'grant_status', multiplier: 'Turnover multiplier',
'grant_status NOT_SENT': 'NOT_SENT', external_transaction_id: 'Order number',
'grant_status SENT_PENDING': 'SENT_PENDING', playx_transaction_id: 'PlayX transaction ID',
'grant_status ACCEPTED': 'ACCEPTED', grant_status: 'Grant status',
'grant_status FAILED_RETRYABLE': 'FAILED_RETRYABLE', 'grant_status NOT_SENT': 'Not sent',
'grant_status FAILED_FINAL': 'FAILED_FINAL', 'grant_status SENT_PENDING': 'Sent (queued)',
fail_reason: 'fail_reason', 'grant_status ACCEPTED': 'Accepted',
reject_reason: 'reject_reason', 'grant_status FAILED_RETRYABLE': 'Failed (retryable)',
shipping_company: 'shipping_company', 'grant_status FAILED_FINAL': 'Failed (final)',
shipping_no: 'shipping_no', 'grant_status ---': '—',
receiver_name: 'receiver_name', fail_reason: 'Failure reason',
receiver_phone: 'receiver_phone', reject_reason: 'Rejection reason',
receiver_address: 'receiver_address', shipping_company: 'Carrier',
mall_address_id: 'mall_address_id', shipping_no: 'Tracking number',
create_time: 'create_time', receiver_name: 'Recipient name',
update_time: 'update_time', receiver_phone: 'Recipient phone',
'quick Search Fields': 'ID', receiver_address: 'Shipping address',
mall_address_id: 'Address ID',
start_time: 'Redemption time',
end_time: 'Collection end time',
create_time: 'Created at',
update_time: 'Updated at',
'quick Search Fields': 'Order ID',
} }

View File

@@ -23,6 +23,7 @@ export default {
'grant_status ACCEPTED': 'ACCEPTED', 'grant_status ACCEPTED': 'ACCEPTED',
'grant_status FAILED_RETRYABLE': 'FAILED_RETRYABLE', 'grant_status FAILED_RETRYABLE': 'FAILED_RETRYABLE',
'grant_status FAILED_FINAL': 'FAILED_FINAL', 'grant_status FAILED_FINAL': 'FAILED_FINAL',
'grant_status ---': '---',
fail_reason: 'fail_reason', fail_reason: 'fail_reason',
reject_reason: 'reject_reason', reject_reason: 'reject_reason',
shipping_company: 'shipping_company', shipping_company: 'shipping_company',

View File

@@ -0,0 +1,119 @@
/**
* Admin menu titles (admin_rule.name → menu.names.{name with / as _})
*/
export default {
names: {
dashboard: 'Dashboard',
dashboard_index: 'Browse',
dashboard_dashboard: 'Dashboard',
auth: 'Access control',
auth_group: 'Admin groups',
auth_group_index: 'Browse',
auth_group_add: 'Add',
auth_group_edit: 'Edit',
auth_group_del: 'Delete',
auth_admin: 'Administrators',
auth_admin_index: 'Browse',
auth_admin_add: 'Add',
auth_admin_edit: 'Edit',
auth_admin_del: 'Delete',
auth_rule: 'Menu rules',
auth_rule_index: 'Browse',
auth_rule_add: 'Add',
auth_rule_edit: 'Edit',
auth_rule_del: 'Delete',
auth_rule_sortable: 'Sort',
auth_adminLog: 'Admin logs',
auth_adminLog_index: 'Browse',
user: 'Members',
user_user: 'Members',
user_user_index: 'Browse',
user_user_add: 'Add',
user_user_edit: 'Edit',
user_user_del: 'Delete',
user_group: 'Member groups',
user_group_index: 'Browse',
user_group_add: 'Add',
user_group_edit: 'Edit',
user_group_del: 'Delete',
user_rule: 'Member rules',
user_rule_index: 'Browse',
user_rule_add: 'Add',
user_rule_edit: 'Edit',
user_rule_del: 'Delete',
user_rule_sortable: 'Sort',
user_moneyLog: 'Balance logs',
user_moneyLog_index: 'Browse',
user_moneyLog_add: 'Add',
user_scoreLog: 'Points logs',
user_scoreLog_index: 'Browse',
user_scoreLog_add: 'Add',
routine: 'General',
routine_config: 'System config',
routine_config_index: 'Browse',
routine_config_edit: 'Edit',
routine_config_add: 'Add',
routine_config_del: 'Delete',
routine_attachment: 'Attachments',
routine_attachment_index: 'Browse',
routine_attachment_edit: 'Edit',
routine_attachment_del: 'Delete',
routine_adminInfo: 'Profile',
routine_adminInfo_index: 'Browse',
routine_adminInfo_edit: 'Edit',
security: 'Data security',
security_dataRecycleLog: 'Recycle bin',
security_dataRecycleLog_index: 'Browse',
security_dataRecycleLog_del: 'Delete',
security_dataRecycleLog_restore: 'Restore',
security_dataRecycleLog_info: 'Details',
security_sensitiveDataLog: 'Sensitive data logs',
security_sensitiveDataLog_index: 'Browse',
security_sensitiveDataLog_del: 'Delete',
security_sensitiveDataLog_rollback: 'Rollback',
security_sensitiveDataLog_info: 'Details',
security_dataRecycle: 'Recycle rules',
security_dataRecycle_index: 'Browse',
security_dataRecycle_add: 'Add',
security_dataRecycle_edit: 'Edit',
security_dataRecycle_del: 'Delete',
security_sensitiveData: 'Sensitive field rules',
security_sensitiveData_index: 'Browse',
security_sensitiveData_add: 'Add',
security_sensitiveData_edit: 'Edit',
security_sensitiveData_del: 'Delete',
buildadmin: 'BuildAdmin',
buildadmin_buildadmin: 'BuildAdmin',
moduleStore_moduleStore: 'Module store',
moduleStore_moduleStore_index: 'Browse',
moduleStore_moduleStore_install: 'Install',
moduleStore_moduleStore_changeState: 'Change state',
moduleStore_moduleStore_uninstall: 'Uninstall',
moduleStore_moduleStore_update: 'Update',
crud_crud: 'CRUD generator',
crud_crud_index: 'Browse',
crud_crud_generate: 'Generate',
crud_crud_delete: 'Delete',
mall: 'Points mall',
mall_userAsset: 'User assets',
mall_userAsset_index: 'Browse',
mall_userAsset_edit: 'Edit',
mall_userAsset_del: 'Delete',
mall_address: 'Shipping addresses',
mall_order: 'Orders',
mall_order_add: 'Add',
mall_order_edit: 'Edit',
mall_order_del: 'Delete',
mall_order_approve: 'Approve',
mall_dailyPush: 'Daily push',
mall_claimLog: 'Claim log',
mall_item: 'Products',
mall_playxOrder: 'PlayX orders',
mall_playxCenter: 'PlayX center',
mall_playxClaimLog: 'PlayX claim log',
mall_playxDailyPush: 'PlayX daily push',
mall_playxUserAsset: 'PlayX user assets',
mall_pintsOrder: 'Points orders',
mall_redemptionOrder: 'Redemption orders',
},
}

View File

@@ -2,7 +2,10 @@
* 后台公共语言包 * 后台公共语言包
* 覆盖风险:请避免使用页面语言包的目录名、文件名作为翻译 key * 覆盖风险:请避免使用页面语言包的目录名、文件名作为翻译 key
*/ */
import menu from './zh-cn/menu'
export default { export default {
menu,
Balance: '余额', Balance: '余额',
Integral: '积分', Integral: '积分',
Connection: '连接标识', Connection: '连接标识',

View File

@@ -1,4 +1,7 @@
export default { export default {
approve: '审核',
manual_retry: '手动重试',
retry_confirm: '确认将该订单加入重试队列?',
id: 'ID', id: 'ID',
user_id: '用户ID', user_id: '用户ID',
type: '类型', type: '类型',
@@ -17,12 +20,13 @@ export default {
multiplier: '流水倍数', multiplier: '流水倍数',
external_transaction_id: '订单号', external_transaction_id: '订单号',
playx_transaction_id: 'PlayX流水号', playx_transaction_id: 'PlayX流水号',
grant_status: '发放子状态', grant_status: '推送playx状态',
'grant_status NOT_SENT': '未发送', 'grant_status NOT_SENT': '未发送',
'grant_status SENT_PENDING': '已发送排队', 'grant_status SENT_PENDING': '已发送排队',
'grant_status ACCEPTED': '已接收(accepted)', 'grant_status ACCEPTED': '已接收',
'grant_status FAILED_RETRYABLE': '失败可重试', 'grant_status FAILED_RETRYABLE': '失败可重试',
'grant_status FAILED_FINAL': '失败最终', 'grant_status FAILED_FINAL': '失败最终',
'grant_status ---': '---',
fail_reason: '失败原因', fail_reason: '失败原因',
reject_reason: '驳回原因', reject_reason: '驳回原因',
shipping_company: '物流公司', shipping_company: '物流公司',
@@ -31,8 +35,9 @@ export default {
receiver_phone: '收货电话', receiver_phone: '收货电话',
receiver_address: '收货地址', receiver_address: '收货地址',
mall_address_id: '地址ID', mall_address_id: '地址ID',
start_time: '兑换时间',
end_time: '领取结束时间',
create_time: '创建时间', create_time: '创建时间',
update_time: '修改时间', update_time: '修改时间',
'quick Search Fields': 'ID', 'quick Search Fields': 'ID',
} }

View File

@@ -1,6 +1,6 @@
export default { export default {
title: 'PlayX 对接中心', title: 'PlayX 对接中心',
desc: '集中管理积分商城与 PlayX 的订单、推送、领取与资产数据。建议优先处理“发放子状态=失败可重试”的订单。', desc: '集中管理积分商城与 PlayX 的订单、推送、领取与资产数据。建议优先处理“推送playx状态=失败可重试”的订单。',
orders: '统一订单', orders: '统一订单',
dailyPush: '每日推送', dailyPush: '每日推送',
claimLog: '领取记录', claimLog: '领取记录',

View File

@@ -17,12 +17,13 @@ export default {
multiplier: '流水倍数', multiplier: '流水倍数',
external_transaction_id: '订单号', external_transaction_id: '订单号',
playx_transaction_id: 'PlayX流水号', playx_transaction_id: 'PlayX流水号',
grant_status: '发放子状态', grant_status: '推送playx状态',
'grant_status NOT_SENT': '未发送', 'grant_status NOT_SENT': '未发送',
'grant_status SENT_PENDING': '已发送排队', 'grant_status SENT_PENDING': '已发送排队',
'grant_status ACCEPTED': '已接收(accepted)', 'grant_status ACCEPTED': '已接收(accepted)',
'grant_status FAILED_RETRYABLE': '失败可重试', 'grant_status FAILED_RETRYABLE': '失败可重试',
'grant_status FAILED_FINAL': '失败最终', 'grant_status FAILED_FINAL': '失败最终',
'grant_status ---': '---',
fail_reason: '失败原因', fail_reason: '失败原因',
reject_reason: '驳回原因', reject_reason: '驳回原因',
shipping_company: '物流公司', shipping_company: '物流公司',

View File

@@ -0,0 +1,120 @@
/**
* 后台菜单标题(与 admin_rule.name 对应menu.names.{name 中 / 改为 _}
*/
export default {
names: {
/** version202 后菜单 name 为 dashboard不再使用 dashboard/dashboard */
dashboard: '控制台',
dashboard_index: '查看',
dashboard_dashboard: '控制台',
auth: '权限管理',
auth_group: '角色组管理',
auth_group_index: '查看',
auth_group_add: '添加',
auth_group_edit: '编辑',
auth_group_del: '删除',
auth_admin: '管理员管理',
auth_admin_index: '查看',
auth_admin_add: '添加',
auth_admin_edit: '编辑',
auth_admin_del: '删除',
auth_rule: '菜单规则管理',
auth_rule_index: '查看',
auth_rule_add: '添加',
auth_rule_edit: '编辑',
auth_rule_del: '删除',
auth_rule_sortable: '快速排序',
auth_adminLog: '管理员日志管理',
auth_adminLog_index: '查看',
user: '会员管理',
user_user: '会员管理',
user_user_index: '查看',
user_user_add: '添加',
user_user_edit: '编辑',
user_user_del: '删除',
user_group: '会员分组管理',
user_group_index: '查看',
user_group_add: '添加',
user_group_edit: '编辑',
user_group_del: '删除',
user_rule: '会员规则管理',
user_rule_index: '查看',
user_rule_add: '添加',
user_rule_edit: '编辑',
user_rule_del: '删除',
user_rule_sortable: '快速排序',
user_moneyLog: '会员余额管理',
user_moneyLog_index: '查看',
user_moneyLog_add: '添加',
user_scoreLog: '会员积分管理',
user_scoreLog_index: '查看',
user_scoreLog_add: '添加',
routine: '常规管理',
routine_config: '系统配置',
routine_config_index: '查看',
routine_config_edit: '编辑',
routine_config_add: '添加',
routine_config_del: '删除',
routine_attachment: '附件管理',
routine_attachment_index: '查看',
routine_attachment_edit: '编辑',
routine_attachment_del: '删除',
routine_adminInfo: '个人资料',
routine_adminInfo_index: '查看',
routine_adminInfo_edit: '编辑',
security: '数据安全管理',
security_dataRecycleLog: '数据回收站',
security_dataRecycleLog_index: '查看',
security_dataRecycleLog_del: '删除',
security_dataRecycleLog_restore: '还原',
security_dataRecycleLog_info: '查看详情',
security_sensitiveDataLog: '敏感数据修改记录',
security_sensitiveDataLog_index: '查看',
security_sensitiveDataLog_del: '删除',
security_sensitiveDataLog_rollback: '回滚',
security_sensitiveDataLog_info: '查看详情',
security_dataRecycle: '数据回收规则管理',
security_dataRecycle_index: '查看',
security_dataRecycle_add: '添加',
security_dataRecycle_edit: '编辑',
security_dataRecycle_del: '删除',
security_sensitiveData: '敏感字段规则管理',
security_sensitiveData_index: '查看',
security_sensitiveData_add: '添加',
security_sensitiveData_edit: '编辑',
security_sensitiveData_del: '删除',
buildadmin: 'BuildAdmin',
buildadmin_buildadmin: 'BuildAdmin',
moduleStore_moduleStore: '模块市场',
moduleStore_moduleStore_index: '查看',
moduleStore_moduleStore_install: '安装',
moduleStore_moduleStore_changeState: '调整状态',
moduleStore_moduleStore_uninstall: '卸载',
moduleStore_moduleStore_update: '更新',
crud_crud: 'CRUD代码生成',
crud_crud_index: '查看',
crud_crud_generate: '生成',
crud_crud_delete: '删除',
mall: '积分商城',
mall_userAsset: '用户资产',
mall_userAsset_index: '查看',
mall_userAsset_edit: '编辑',
mall_userAsset_del: '删除',
mall_address: '收货地址管理',
mall_order: '统一订单',
mall_order_add: '新增',
mall_order_edit: '编辑',
mall_order_del: '删除',
mall_order_approve: '审核通过',
mall_dailyPush: '每日推送',
mall_claimLog: '领取记录',
mall_item: '商品管理',
mall_playxOrder: 'PlayX订单',
mall_playxCenter: 'PlayX中心',
mall_playxClaimLog: 'PlayX领取记录',
mall_playxDailyPush: 'PlayX每日推送',
mall_playxUserAsset: 'PlayX用户资产',
mall_pintsOrder: '积分订单',
mall_redemptionOrder: '兑换订单',
},
}

View File

@@ -4,7 +4,7 @@
<el-sub-menu @click="onClickSubMenu(menu)" :index="getMenuKey(menu)" :key="getMenuKey(menu)"> <el-sub-menu @click="onClickSubMenu(menu)" :index="getMenuKey(menu)" :key="getMenuKey(menu)">
<template #title> <template #title>
<Icon :color="config.getColorVal('menuColor')" :name="menu.meta?.icon ? menu.meta?.icon : config.layout.menuDefaultIcon" /> <Icon :color="config.getColorVal('menuColor')" :name="menu.meta?.icon ? menu.meta?.icon : config.layout.menuDefaultIcon" />
<span>{{ menu.meta?.title ? menu.meta?.title : $t('noTitle') }}</span> <span>{{ menuTitleFromRoute(menu) }}</span>
</template> </template>
<MenuTree :extends="{ ...props.extends, level: props.extends.level + 1 }" :menus="menu.children" /> <MenuTree :extends="{ ...props.extends, level: props.extends.level + 1 }" :menus="menu.children" />
</el-sub-menu> </el-sub-menu>
@@ -12,7 +12,7 @@
<template v-else> <template v-else>
<el-menu-item @click="onClickMenu(menu)" :index="getMenuKey(menu)" :key="getMenuKey(menu)"> <el-menu-item @click="onClickMenu(menu)" :index="getMenuKey(menu)" :key="getMenuKey(menu)">
<Icon :color="config.getColorVal('menuColor')" :name="menu.meta?.icon ? menu.meta?.icon : config.layout.menuDefaultIcon" /> <Icon :color="config.getColorVal('menuColor')" :name="menu.meta?.icon ? menu.meta?.icon : config.layout.menuDefaultIcon" />
<span>{{ menu.meta?.title ? menu.meta?.title : $t('noTitle') }}</span> <span>{{ menuTitleFromRoute(menu) }}</span>
</el-menu-item> </el-menu-item>
</template> </template>
</template> </template>
@@ -23,6 +23,7 @@ import { ElNotification } from 'element-plus'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import type { RouteRecordRaw } from 'vue-router' import type { RouteRecordRaw } from 'vue-router'
import { useConfig } from '/@/stores/config' import { useConfig } from '/@/stores/config'
import { menuTitleFromRoute } from '/@/utils/menuI18n'
import { getFirstRoute, getMenuKey, onClickMenu } from '/@/utils/router' import { getFirstRoute, getMenuKey, onClickMenu } from '/@/utils/router'
const { t } = useI18n() const { t } = useI18n()

View File

@@ -9,7 +9,7 @@
:ref="tabsRefs.set" :ref="tabsRefs.set"
:key="idx" :key="idx"
> >
{{ item.meta.title }} {{ menuTitleFromRoute(item) }}
<transition @after-leave="selectNavTab(tabsRefs[navTabs.state.activeIndex])" name="el-fade-in"> <transition @after-leave="selectNavTab(tabsRefs[navTabs.state.activeIndex])" name="el-fade-in">
<Icon v-show="navTabs.state.tabsView.length > 1" class="close-icon" @click.stop="closeTab(item)" size="15" name="el-icon-Close" /> <Icon v-show="navTabs.state.tabsView.length > 1" class="close-icon" @click.stop="closeTab(item)" size="15" name="el-icon-Close" />
</transition> </transition>
@@ -29,6 +29,7 @@ import type { ContextMenuItem, ContextMenuItemClickEmitArg } from '/@/components
import useCurrentInstance from '/@/utils/useCurrentInstance' import useCurrentInstance from '/@/utils/useCurrentInstance'
import Contextmenu from '/@/components/contextmenu/index.vue' import Contextmenu from '/@/components/contextmenu/index.vue'
import horizontalScroll from '/@/utils/horizontalScroll' import horizontalScroll from '/@/utils/horizontalScroll'
import { menuTitleFromRoute } from '/@/utils/menuI18n'
import { getFirstRoute, routePush } from '/@/utils/router' import { getFirstRoute, routePush } from '/@/utils/router'
import { adminBaseRoutePath } from '/@/router/static/adminBase' import { adminBaseRoutePath } from '/@/router/static/adminBase'

33
web/src/utils/menuI18n.ts Normal file
View File

@@ -0,0 +1,33 @@
import type { RouteLocationNormalized, RouteRecordRaw } from 'vue-router'
import { i18n } from '/@/lang/index'
/**
* 后台菜单/标签标题:优先按路由 name对应 admin_rule.name匹配 menu.names.*
*/
export function menuI18nKeyFromName(name: string | symbol | null | undefined): string {
if (name == null || name === '') {
return ''
}
const n = String(name).trim()
if (!n) {
return ''
}
return `menu.names.${n.replace(/\//g, '_')}`
}
export function menuTitleFromName(name: string | symbol | null | undefined, fallback?: string): string {
const key = menuI18nKeyFromName(name)
if (key && i18n.global.te(key)) {
return String(i18n.global.t(key))
}
if (fallback && i18n.global.te(fallback)) {
return String(i18n.global.t(fallback))
}
return fallback || ''
}
export function menuTitleFromRoute(route: RouteRecordRaw | RouteLocationNormalized): string {
const name = route.name
const metaTitle = route.meta && typeof route.meta.title === 'string' ? route.meta.title : ''
return menuTitleFromName(name, metaTitle) || metaTitle || String(i18n.global.t('noTitle'))
}

View File

@@ -34,7 +34,7 @@ const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete']).map((btn)
btn.name === 'edit' btn.name === 'edit'
? { ? {
...btn, ...btn,
title: '审核', title: t('mall.order.approve'),
type: 'primary', type: 'primary',
class: 'table-row-edit', class: 'table-row-edit',
icon: 'fa fa-check', icon: 'fa fa-check',
@@ -125,6 +125,15 @@ const baTable = new baTableClass(
label: t('mall.order.grant_status'), label: t('mall.order.grant_status'),
prop: 'grant_status', prop: 'grant_status',
align: 'center', align: 'center',
minWidth: 100,
custom: {
NOT_SENT: 'info',
SENT_PENDING: 'primary',
ACCEPTED: 'primary',
FAILED_RETRYABLE: 'error',
FAILED_FINAL: 'error',
'---': 'info',
},
operator: 'eq', operator: 'eq',
sortable: false, sortable: false,
render: 'tag', render: 'tag',
@@ -134,6 +143,7 @@ const baTable = new baTableClass(
ACCEPTED: t('mall.order.grant_status ACCEPTED'), ACCEPTED: t('mall.order.grant_status ACCEPTED'),
FAILED_RETRYABLE: t('mall.order.grant_status FAILED_RETRYABLE'), FAILED_RETRYABLE: t('mall.order.grant_status FAILED_RETRYABLE'),
FAILED_FINAL: t('mall.order.grant_status FAILED_FINAL'), FAILED_FINAL: t('mall.order.grant_status FAILED_FINAL'),
'---': t('mall.order.grant_status ---'),
}, },
}, },
{ {
@@ -200,6 +210,24 @@ const baTable = new baTableClass(
operatorPlaceholder: t('Fuzzy query'), 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.mall_address_id'), prop: 'mall_address_id', align: 'center', width: 100, operator: 'eq', sortable: false },
{
label: t('mall.order.start_time'),
prop: 'start_time',
align: 'center',
operator: 'RANGE',
comSearchRender: 'datetime',
sortable: 'custom',
width: 160,
},
{
label: t('mall.order.end_time'),
prop: 'end_time',
align: 'center',
operator: 'RANGE',
comSearchRender: 'datetime',
sortable: 'custom',
width: 160,
},
{ {
label: t('mall.order.create_time'), label: t('mall.order.create_time'),
prop: 'create_time', prop: 'create_time',
@@ -215,6 +243,7 @@ const baTable = new baTableClass(
label: t('mall.order.update_time'), label: t('mall.order.update_time'),
prop: 'update_time', prop: 'update_time',
align: 'center', align: 'center',
show: false,
render: 'datetime', render: 'datetime',
operator: 'RANGE', operator: 'RANGE',
comSearchRender: 'datetime', comSearchRender: 'datetime',
@@ -225,7 +254,7 @@ const baTable = new baTableClass(
{ {
label: t('Operate'), label: t('Operate'),
align: 'center', align: 'center',
width: 80, width: 120,
fixed: 'right', fixed: 'right',
render: 'buttons', render: 'buttons',
buttons: [ buttons: [
@@ -233,17 +262,20 @@ const baTable = new baTableClass(
{ {
render: 'confirmButton', render: 'confirmButton',
name: 'retry', name: 'retry',
title: 'Retry', title: t('mall.order.manual_retry'),
text: '手动重试', text: '',
type: 'warning', type: 'primary',
icon: '', class: 'table-row-edit',
icon: 'fa fa-refresh',
display: (row: TableRow) => display: (row: TableRow) =>
(row.type === 'BONUS' || row.type === 'WITHDRAW') && row.grant_status === 'FAILED_RETRYABLE' && row.status === 'PENDING', row.type === 'BONUS' &&
row.status === 'PENDING' &&
['NOT_SENT', 'SENT_PENDING', 'FAILED_RETRYABLE', 'FAILED_FINAL'].includes(String(row.grant_status)),
popconfirm: { popconfirm: {
title: '确认将该订单加入重试队列?', title: t('mall.order.retry_confirm'),
confirmButtonText: '确认', confirmButtonText: t('Confirm'),
cancelButtonText: '取消', cancelButtonText: t('Cancel'),
confirmButtonType: 'warning', confirmButtonType: 'primary',
}, },
click: async (row: TableRow) => { click: async (row: TableRow) => {
await createAxios( await createAxios(

View File

@@ -57,7 +57,7 @@
</template> </template>
<template v-else-if="usePagedActions"> <template v-else-if="usePagedActions">
<template v-if="action === 'approveShip'"> <template v-if="action === 'approveShip' && isPhysical">
<FormItem <FormItem
:label="t('mall.order.shipping_company')" :label="t('mall.order.shipping_company')"
type="string" type="string"
@@ -75,6 +75,7 @@
</template> </template>
<template v-else-if="action === 'reject'"> <template v-else-if="action === 'reject'">
<FormItem <FormItem
v-if="isPhysical"
:label="t('mall.order.reject_reason')" :label="t('mall.order.reject_reason')"
type="textarea" type="textarea"
v-model="baTable.form.items!.reject_reason" v-model="baTable.form.items!.reject_reason"
@@ -83,6 +84,9 @@
@keyup.enter.stop="" @keyup.enter.stop=""
:placeholder="t('Please input field', { field: t('mall.order.reject_reason') })" :placeholder="t('Please input field', { field: t('mall.order.reject_reason') })"
/> />
<el-alert v-else type="info" :closable="false" show-icon>
确认后将驳回该订单并退回积分红利/提现订单无需填写驳回原因
</el-alert>
</template> </template>
</template> </template>
@@ -97,29 +101,31 @@
:input-attr="{ content: { PENDING: t('mall.order.status PENDING'), COMPLETED: t('mall.order.status COMPLETED'), SHIPPED: t('mall.order.status SHIPPED'), REJECTED: t('mall.order.status REJECTED') } }" :input-attr="{ content: { PENDING: t('mall.order.status PENDING'), COMPLETED: t('mall.order.status COMPLETED'), SHIPPED: t('mall.order.status SHIPPED'), REJECTED: t('mall.order.status REJECTED') } }"
:placeholder="t('Please select field', { field: t('mall.order.status') })" :placeholder="t('Please select field', { field: t('mall.order.status') })"
/> />
<FormItem <template v-if="isPhysical">
:label="t('mall.order.shipping_company')" <FormItem
type="string" :label="t('mall.order.shipping_company')"
v-model="baTable.form.items!.shipping_company" type="string"
prop="shipping_company" v-model="baTable.form.items!.shipping_company"
:placeholder="t('Please input field', { field: t('mall.order.shipping_company') })" prop="shipping_company"
/> :placeholder="t('Please input field', { field: t('mall.order.shipping_company') })"
<FormItem />
:label="t('mall.order.shipping_no')" <FormItem
type="string" :label="t('mall.order.shipping_no')"
v-model="baTable.form.items!.shipping_no" type="string"
prop="shipping_no" v-model="baTable.form.items!.shipping_no"
:placeholder="t('Please input field', { field: t('mall.order.shipping_no') })" prop="shipping_no"
/> :placeholder="t('Please input field', { field: t('mall.order.shipping_no') })"
<FormItem />
:label="t('mall.order.reject_reason')" <FormItem
type="textarea" :label="t('mall.order.reject_reason')"
v-model="baTable.form.items!.reject_reason" type="textarea"
prop="reject_reason" v-model="baTable.form.items!.reject_reason"
:input-attr="{ rows: 3 }" prop="reject_reason"
@keyup.enter.stop="" :input-attr="{ rows: 3 }"
:placeholder="t('Please input field', { field: t('mall.order.reject_reason') })" @keyup.enter.stop=""
/> :placeholder="t('Please input field', { field: t('mall.order.reject_reason') })"
/>
</template>
</template> </template>
</el-form> </el-form>
</div> </div>
@@ -159,7 +165,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject, reactive, ref, useTemplateRef, watch } from 'vue' import { computed, inject, ref, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useConfig } from '/@/stores/config' import { useConfig } from '/@/stores/config'
import baTableClass from '/@/utils/baTable' import baTableClass from '/@/utils/baTable'
@@ -261,7 +267,10 @@ const submitShip = async () => {
const submitReject = async () => { const submitReject = async () => {
const id = baTable.form.items?.id const id = baTable.form.items?.id
const rejectReason = (baTable.form.items?.reject_reason || '').toString().trim() const rejectReason = (baTable.form.items?.reject_reason || '').toString().trim()
if (!id || rejectReason === '') { if (!id) {
return
}
if (isPhysical.value && rejectReason === '') {
ElMessage.error('请填写驳回原因') ElMessage.error('请填写驳回原因')
return return
} }
@@ -279,10 +288,15 @@ const submitReject = async () => {
} }
} }
const rules: Partial<Record<string, FormItemRule[]>> = reactive({ const rules = computed<Partial<Record<string, FormItemRule[]>>>(() => {
shipping_company: [buildValidatorData({ name: 'required', title: t('mall.order.shipping_company') })], if (!isPhysical.value) {
shipping_no: [buildValidatorData({ name: 'required', title: t('mall.order.shipping_no') })], return {}
reject_reason: [buildValidatorData({ name: 'required', title: t('mall.order.reject_reason') })], }
return {
shipping_company: [buildValidatorData({ name: 'required', title: t('mall.order.shipping_company') })],
shipping_no: [buildValidatorData({ name: 'required', title: t('mall.order.shipping_no') })],
reject_reason: [buildValidatorData({ name: 'required', title: t('mall.order.reject_reason') })],
}
}) })
</script> </script>

View File

@@ -83,6 +83,7 @@ const baTable = new baTableClass(
ACCEPTED: t('mall.playxOrder.grant_status ACCEPTED'), ACCEPTED: t('mall.playxOrder.grant_status ACCEPTED'),
FAILED_RETRYABLE: t('mall.playxOrder.grant_status FAILED_RETRYABLE'), FAILED_RETRYABLE: t('mall.playxOrder.grant_status FAILED_RETRYABLE'),
FAILED_FINAL: t('mall.playxOrder.grant_status FAILED_FINAL'), FAILED_FINAL: t('mall.playxOrder.grant_status FAILED_FINAL'),
'---': t('mall.playxOrder.grant_status ---'),
}, },
}, },
{ {
@@ -116,7 +117,9 @@ const baTable = new baTableClass(
type: 'warning', type: 'warning',
icon: '', icon: '',
display: (row: TableRow) => display: (row: TableRow) =>
(row.type === 'BONUS' || row.type === 'WITHDRAW') && row.grant_status === 'FAILED_RETRYABLE' && row.status === 'PENDING', row.type === 'BONUS' &&
row.status === 'PENDING' &&
['NOT_SENT', 'SENT_PENDING', 'FAILED_RETRYABLE', 'FAILED_FINAL'].includes(String(row.grant_status)),
popconfirm: { popconfirm: {
title: '确认将该订单加入重试队列?', title: '确认将该订单加入重试队列?',
confirmButtonText: '确认', confirmButtonText: '确认',
@@ -126,7 +129,7 @@ const baTable = new baTableClass(
click: async (row: TableRow) => { click: async (row: TableRow) => {
await createAxios( await createAxios(
{ {
url: '/admin/mall.PlayxOrder/retry', url: '/admin/mall.Order/retry',
method: 'post', method: 'post',
data: { data: {
id: row.id, id: row.id,