Compare commits
11 Commits
520e950dc5
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 4cf84ca083 | |||
| 9f6358a3f2 | |||
| 0fdc1e2e88 | |||
| 186af5a55f | |||
| 6bec4e7758 | |||
| f9e4e61d93 | |||
| 8b6727dac1 | |||
| b97d33a24f | |||
| 6f12afcd10 | |||
| 5ab85d1d53 | |||
| 941f0f4a8c |
13
.env-example
13
.env-example
@@ -35,3 +35,16 @@ PLAYX_SESSION_EXPIRE_SECONDS=3600
|
|||||||
# PlayX API(商城调用 PlayX 时使用)
|
# PlayX API(商城调用 PlayX 时使用)
|
||||||
PLAYX_API_BASE_URL=
|
PLAYX_API_BASE_URL=
|
||||||
PLAYX_API_SECRET_KEY=
|
PLAYX_API_SECRET_KEY=
|
||||||
|
|
||||||
|
# 推送订单url
|
||||||
|
PLAYX_ANGPOW_IMPORT_BASE_URL=https://ss2-staging2.ttwd8.com
|
||||||
|
# 推送订单接口
|
||||||
|
PLAYX_ANGPOW_IMPORT_PATH=/cashmarket/v3/merchant-api/angpow-imports
|
||||||
|
# 商户编码merchant_code
|
||||||
|
PLAYX_ANGPOW_MERCHANT_CODE=plx
|
||||||
|
# HMAC 密钥(与对端一致)
|
||||||
|
PLAYX_ANGPOW_IMPORT_AUTH_KEY=
|
||||||
|
# HTTPS:CA 证书包绝对路径(推荐下载 https://curl.se/ca/cacert.pem 后填写,解决 cURL error 60)
|
||||||
|
PLAYX_ANGPOW_IMPORT_CACERT=
|
||||||
|
# 是否校验 SSL(1=校验;0=不校验,仅本地调试,生产勿用)
|
||||||
|
PLAYX_ANGPOW_IMPORT_VERIFY_SSL=1
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
];
|
];
|
||||||
@@ -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' => '订单状态须为处理中',
|
||||||
];
|
];
|
||||||
@@ -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]);
|
||||||
@@ -679,6 +679,210 @@ class Playx extends Api
|
|||||||
return $this->success('', ['list' => $list->toArray()]);
|
return $this->success('', ['list' => $list->toArray()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 积分流水(领取/兑换/退回)
|
||||||
|
* GET /api/v1/mall/pointsLogs
|
||||||
|
*
|
||||||
|
* 鉴权:token / session_id / user_id(同 assets/orders)
|
||||||
|
*
|
||||||
|
* 参数:
|
||||||
|
* - limit: 每页条数(默认 20,最大 100)
|
||||||
|
* - cursor: 游标(上一页返回的 next_cursor)
|
||||||
|
* - direction: IN | OUT(可选,不传返回全部)
|
||||||
|
*/
|
||||||
|
public function pointsLogs(Request $request): Response
|
||||||
|
{
|
||||||
|
$response = $this->initializeApi($request);
|
||||||
|
if ($response !== null) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
|
||||||
|
if ($assetId === null) {
|
||||||
|
return $this->error(__('Token expiration'), null, 0, ['statusCode' => 401]);
|
||||||
|
}
|
||||||
|
$asset = $this->getAssetById($assetId);
|
||||||
|
if (!$asset || ($asset->playx_user_id ?? '') === '') {
|
||||||
|
return $this->success('', [
|
||||||
|
'list' => [],
|
||||||
|
'next_cursor' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$playxUserId = $asset->playx_user_id;
|
||||||
|
|
||||||
|
$limit = $request->get('limit', $request->post('limit', '20'));
|
||||||
|
if (!is_string($limit) || $limit === '' || !ctype_digit($limit) || $limit === '0') {
|
||||||
|
$limit = '20';
|
||||||
|
}
|
||||||
|
if (strlen($limit) > 3 || $limit > '100') {
|
||||||
|
$limit = '100';
|
||||||
|
}
|
||||||
|
|
||||||
|
$cursor = $request->get('cursor', $request->post('cursor', ''));
|
||||||
|
if (!is_string($cursor)) {
|
||||||
|
$cursor = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$direction = $request->get('direction', $request->post('direction', ''));
|
||||||
|
$direction = is_string($direction) ? strtoupper(trim($direction)) : '';
|
||||||
|
if ($direction !== '' && $direction !== 'IN' && $direction !== 'OUT') {
|
||||||
|
$direction = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = <<<SQL
|
||||||
|
SELECT
|
||||||
|
t.biz_type,
|
||||||
|
t.direction,
|
||||||
|
t.points,
|
||||||
|
t.ts,
|
||||||
|
t.ref_id,
|
||||||
|
t.order_no,
|
||||||
|
t.order_status,
|
||||||
|
t.item_id,
|
||||||
|
t.item_title,
|
||||||
|
t.item_type,
|
||||||
|
t.item_score,
|
||||||
|
t.item_amount,
|
||||||
|
t.item_multiplier,
|
||||||
|
t.item_category,
|
||||||
|
t.item_category_title,
|
||||||
|
t.sort_key
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
'CLAIM' AS biz_type,
|
||||||
|
'IN' AS direction,
|
||||||
|
cl.claimed_amount AS points,
|
||||||
|
cl.create_time AS ts,
|
||||||
|
cl.claim_request_id AS ref_id,
|
||||||
|
'' AS order_no,
|
||||||
|
'' AS order_status,
|
||||||
|
0 AS item_id,
|
||||||
|
'' AS item_title,
|
||||||
|
0 AS item_type,
|
||||||
|
0 AS item_score,
|
||||||
|
0 AS item_amount,
|
||||||
|
0 AS item_multiplier,
|
||||||
|
'' AS item_category,
|
||||||
|
'' AS item_category_title,
|
||||||
|
CONCAT(LPAD(cl.create_time, 10, '0'), '_1_', LPAD(cl.id, 10, '0')) AS sort_key
|
||||||
|
FROM mall_claim_log cl
|
||||||
|
WHERE cl.user_id = :user_id_claim
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
CONCAT('REDEEM_', o.type) AS biz_type,
|
||||||
|
'OUT' AS direction,
|
||||||
|
o.points_cost AS points,
|
||||||
|
o.create_time AS ts,
|
||||||
|
o.external_transaction_id AS ref_id,
|
||||||
|
o.external_transaction_id AS order_no,
|
||||||
|
o.status AS order_status,
|
||||||
|
o.mall_item_id AS item_id,
|
||||||
|
COALESCE(i.title, '') AS item_title,
|
||||||
|
COALESCE(i.type, 0) AS item_type,
|
||||||
|
COALESCE(i.score, 0) AS item_score,
|
||||||
|
COALESCE(i.amount, 0) AS item_amount,
|
||||||
|
COALESCE(i.multiplier, 0) AS item_multiplier,
|
||||||
|
COALESCE(i.category, '') AS item_category,
|
||||||
|
COALESCE(i.category_title, '') AS item_category_title,
|
||||||
|
CONCAT(LPAD(o.create_time, 10, '0'), '_2_', LPAD(o.id, 10, '0')) AS sort_key
|
||||||
|
FROM mall_order o
|
||||||
|
LEFT JOIN mall_item i ON i.id = o.mall_item_id
|
||||||
|
WHERE o.user_id = :user_id_redeem AND o.points_cost > 0
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
'REFUND' AS biz_type,
|
||||||
|
'IN' AS direction,
|
||||||
|
o.points_cost AS points,
|
||||||
|
o.update_time AS ts,
|
||||||
|
o.external_transaction_id AS ref_id,
|
||||||
|
o.external_transaction_id AS order_no,
|
||||||
|
o.status AS order_status,
|
||||||
|
o.mall_item_id AS item_id,
|
||||||
|
COALESCE(i.title, '') AS item_title,
|
||||||
|
COALESCE(i.type, 0) AS item_type,
|
||||||
|
COALESCE(i.score, 0) AS item_score,
|
||||||
|
COALESCE(i.amount, 0) AS item_amount,
|
||||||
|
COALESCE(i.multiplier, 0) AS item_multiplier,
|
||||||
|
COALESCE(i.category, '') AS item_category,
|
||||||
|
COALESCE(i.category_title, '') AS item_category_title,
|
||||||
|
CONCAT(LPAD(o.update_time, 10, '0'), '_3_', LPAD(o.id, 10, '0')) AS sort_key
|
||||||
|
FROM mall_order o
|
||||||
|
LEFT JOIN mall_item i ON i.id = o.mall_item_id
|
||||||
|
WHERE o.user_id = :user_id_refund AND o.status = 'REJECTED' AND o.points_cost > 0 AND o.update_time IS NOT NULL
|
||||||
|
) t
|
||||||
|
WHERE 1=1
|
||||||
|
SQL;
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
'user_id_claim' => $playxUserId,
|
||||||
|
'user_id_redeem' => $playxUserId,
|
||||||
|
'user_id_refund' => $playxUserId,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($cursor !== '') {
|
||||||
|
$sql .= "\n AND t.sort_key < :cursor";
|
||||||
|
$params['cursor'] = $cursor;
|
||||||
|
}
|
||||||
|
if ($direction !== '') {
|
||||||
|
$sql .= "\n AND t.direction = :direction";
|
||||||
|
$params['direction'] = $direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= "\nORDER BY t.sort_key DESC";
|
||||||
|
$sql .= "\nLIMIT " . $limit;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$rows = Db::query($sql, $params);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return $this->error($e->getMessage(), null, 0, ['statusCode' => 500]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$list = [];
|
||||||
|
$nextCursor = null;
|
||||||
|
if (is_array($rows)) {
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
if (!is_array($row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$list[] = [
|
||||||
|
'biz_type' => $row['biz_type'] ?? '',
|
||||||
|
'direction' => $row['direction'] ?? '',
|
||||||
|
'points' => $row['points'] ?? 0,
|
||||||
|
'ts' => $row['ts'] ?? null,
|
||||||
|
'ref_id' => $row['ref_id'] ?? '',
|
||||||
|
'order_no' => $row['order_no'] ?? '',
|
||||||
|
'order_status' => $row['order_status'] ?? '',
|
||||||
|
'item_id' => $row['item_id'] ?? 0,
|
||||||
|
'item_title' => $row['item_title'] ?? '',
|
||||||
|
'item_type' => $row['item_type'] ?? 0,
|
||||||
|
'item_score' => $row['item_score'] ?? 0,
|
||||||
|
'mallItem' => ($row['item_id'] ?? 0) ? [
|
||||||
|
'id' => $row['item_id'] ?? 0,
|
||||||
|
'title' => $row['item_title'] ?? '',
|
||||||
|
'type' => $row['item_type'] ?? 0,
|
||||||
|
'score' => $row['item_score'] ?? 0,
|
||||||
|
'amount' => $row['item_amount'] ?? 0,
|
||||||
|
'multiplier' => $row['item_multiplier'] ?? 0,
|
||||||
|
'category' => $row['item_category'] ?? '',
|
||||||
|
'category_title' => $row['item_category_title'] ?? '',
|
||||||
|
] : null,
|
||||||
|
'cursor' => $row['sort_key'] ?? '',
|
||||||
|
];
|
||||||
|
$nextCursor = $row['sort_key'] ?? $nextCursor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success('', [
|
||||||
|
'list' => $list,
|
||||||
|
'next_cursor' => $nextCursor,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 收货地址列表
|
* 收货地址列表
|
||||||
* GET /api/v1/playx/address/list?session_id=xxx
|
* GET /api/v1/playx/address/list?session_id=xxx
|
||||||
@@ -942,6 +1146,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 +1222,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 +1289,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 +1300,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 +1345,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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
85
app/common/library/MallBonusGrantPush.php
Normal file
85
app/common/library/MallBonusGrantPush.php
Normal 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' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
|||||||
328
app/process/AngpowImportJobs.php
Normal file
328
app/process/AngpowImportJobs.php
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\process;
|
||||||
|
|
||||||
|
use app\common\model\MallItem;
|
||||||
|
use app\common\model\MallOrder;
|
||||||
|
use app\common\model\MallUserAsset;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use Workerman\Timer;
|
||||||
|
use Workerman\Worker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Angpow 导入推送任务
|
||||||
|
* - 数据源:mall_order(type=BONUS)
|
||||||
|
* - 推送频率:每 30 秒
|
||||||
|
* - 批量:每次最多 100 条(对方文档限制)
|
||||||
|
* - 幂等:merchant_code + report_date 级别签名;每条订单通过 external_transaction_id 在本地控制只推送一次
|
||||||
|
*/
|
||||||
|
class AngpowImportJobs
|
||||||
|
{
|
||||||
|
private const TIMER_SECONDS = 30;
|
||||||
|
private const BATCH_LIMIT = 100;
|
||||||
|
private const MAX_RETRY = 3;
|
||||||
|
|
||||||
|
protected Client $http;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
// 确保定时任务只在一个 worker 上运行
|
||||||
|
if (!Worker::getAllWorkers()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->http = new Client($this->buildGuzzleOptions());
|
||||||
|
|
||||||
|
Timer::add(self::TIMER_SECONDS, [$this, 'pushPendingOrders']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guzzle 默认校验 HTTPS;Windows 未配置 CA 时会出现 cURL error 60。
|
||||||
|
* 优先使用 PLAYX_ANGPOW_IMPORT_CACERT 指向 cacert.pem;否则可按环境关闭校验(仅开发)。
|
||||||
|
*/
|
||||||
|
private function buildGuzzleOptions(): array
|
||||||
|
{
|
||||||
|
$options = [
|
||||||
|
'timeout' => 20,
|
||||||
|
'http_errors' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
$conf = config('playx.angpow_import');
|
||||||
|
if (!is_array($conf)) {
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
$caFile = $conf['ca_file'] ?? '';
|
||||||
|
if (is_string($caFile) && $caFile !== '' && is_file($caFile)) {
|
||||||
|
$options['verify'] = $caFile;
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
$verifySsl = $conf['verify_ssl'] ?? true;
|
||||||
|
if ($verifySsl === false) {
|
||||||
|
$options['verify'] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pushPendingOrders(): void
|
||||||
|
{
|
||||||
|
$conf = config('playx.angpow_import');
|
||||||
|
if (!is_array($conf)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$baseUrl = $conf['base_url'] ?? '';
|
||||||
|
$path = $conf['path'] ?? '';
|
||||||
|
$merchantCode = $conf['merchant_code'] ?? '';
|
||||||
|
$authKey = $conf['auth_key'] ?? '';
|
||||||
|
|
||||||
|
if (!is_string($baseUrl) || $baseUrl === '' || !is_string($path) || $path === '' || !is_string($merchantCode) || $merchantCode === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!is_string($authKey) || $authKey === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = rtrim($baseUrl, '/') . $path;
|
||||||
|
|
||||||
|
$orders = MallOrder::where('type', MallOrder::TYPE_BONUS)
|
||||||
|
->whereIn('grant_status', [MallOrder::GRANT_NOT_SENT, MallOrder::GRANT_FAILED_RETRYABLE])
|
||||||
|
->where('status', MallOrder::STATUS_PENDING)
|
||||||
|
->where('retry_count', '<', self::MAX_RETRY)
|
||||||
|
->order('id', 'asc')
|
||||||
|
->limit(self::BATCH_LIMIT)
|
||||||
|
->select();
|
||||||
|
|
||||||
|
if ($orders->isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reportDate = strval(time());
|
||||||
|
$signatureInput = 'merchant_code=' . $merchantCode . '&report_date=' . $reportDate;
|
||||||
|
$signature = $this->buildSignature($signatureInput, $authKey);
|
||||||
|
if ($signature === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'merchant_code' => $merchantCode,
|
||||||
|
'report_date' => $reportDate,
|
||||||
|
'angpow' => [],
|
||||||
|
'currency_visual' => [
|
||||||
|
[
|
||||||
|
'currency' => strval($conf['currency'] ?? 'MYR'),
|
||||||
|
'visual_name' => strval($conf['visual_name'] ?? 'Angpow'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$orderIds = [];
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
if (!$order instanceof MallOrder) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$row = $this->buildAngpowRow($order);
|
||||||
|
if ($row === null) {
|
||||||
|
// 构造失败:直接标为可重试失败
|
||||||
|
$this->markFailedAttempt($order, 'Build payload failed');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$payload['angpow'][] = $row;
|
||||||
|
$orderIds[] = $order->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($payload['angpow'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先标记“已尝试发送”,避免并发重复推送;同时只在这里累加 retry_count(一次推送=一次尝试)
|
||||||
|
$now = time();
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
if (!$order instanceof MallOrder) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!in_array($order->id, $orderIds, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$retry = $order->retry_count ?? 0;
|
||||||
|
if (!is_int($retry)) {
|
||||||
|
$retry = is_numeric($retry) ? intval($retry) : 0;
|
||||||
|
}
|
||||||
|
$order->retry_count = $retry + 1;
|
||||||
|
$order->grant_status = MallOrder::GRANT_SENT_PENDING;
|
||||||
|
$order->update_time = $now;
|
||||||
|
$order->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$res = null;
|
||||||
|
$body = '';
|
||||||
|
try {
|
||||||
|
$res = $this->http->post($url, [
|
||||||
|
'headers' => [
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'X-Request-Signature' => $signature,
|
||||||
|
],
|
||||||
|
'json' => $payload,
|
||||||
|
]);
|
||||||
|
$body = strval($res->getBody());
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// 网络/异常:对这一批订单记一次失败尝试
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
if (!$order instanceof MallOrder) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$this->markFailedAttempt($order, $e->getMessage());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($body, true);
|
||||||
|
if (!is_array($data)) {
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
if (!$order instanceof MallOrder) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$this->markFailedAttempt($order, 'Invalid response');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = $data['code'] ?? null;
|
||||||
|
$message = $data['message'] ?? '';
|
||||||
|
$msg = is_string($message) ? $message : 'Request failed';
|
||||||
|
|
||||||
|
// 成功:code=0
|
||||||
|
if ($code === '0' || $code === 0) {
|
||||||
|
MallOrder::whereIn('id', $orderIds)->update([
|
||||||
|
'grant_status' => MallOrder::GRANT_ACCEPTED,
|
||||||
|
'fail_reason' => null,
|
||||||
|
'update_time' => time(),
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 失败:整批视为失败(对方未提供逐条返回)
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
if (!$order instanceof MallOrder) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$this->markFailedAttempt($order, $msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildAngpowRow(MallOrder $order): ?array
|
||||||
|
{
|
||||||
|
$asset = MallUserAsset::where('playx_user_id', $order->user_id)->find();
|
||||||
|
if (!$asset) {
|
||||||
|
if (is_string($order->user_id) && ctype_digit($order->user_id)) {
|
||||||
|
$byId = MallUserAsset::where('id', $order->user_id)->find();
|
||||||
|
if ($byId) {
|
||||||
|
$asset = $byId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$asset || !is_string($asset->playx_user_id ?? null) || strval($asset->playx_user_id) === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$item = null;
|
||||||
|
if ($order->mallItem) {
|
||||||
|
$item = $order->mallItem;
|
||||||
|
} else {
|
||||||
|
$item = MallItem::where('id', $order->mall_item_id)->find();
|
||||||
|
}
|
||||||
|
if (!$item) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $createTime = $order->create_time ?? null;
|
||||||
|
// if (!is_int($createTime)) {
|
||||||
|
// if (is_numeric($createTime)) {
|
||||||
|
// $createTime = intval($createTime);
|
||||||
|
// } else {
|
||||||
|
// $createTime = time();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
$start = gmdate('Y-m-d H:i:s', strtotime($order->start_time));
|
||||||
|
$end = gmdate('Y-m-d H:i:s', strtotime($order->end_time));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'member_login' => strval($asset->playx_user_id),
|
||||||
|
'start_time' => $start,
|
||||||
|
'end_time' => $end,
|
||||||
|
'amount' => $order->amount,
|
||||||
|
'reward_name' => strval($item->title ?? ''),
|
||||||
|
'description' => strval($item->description ?? ''),
|
||||||
|
'member_inbox_message' => 'Congratulations! You received an angpow.',
|
||||||
|
'category' => strval($item->category ?? ''),
|
||||||
|
'category_title' => strval($item->category_title ?? ''),
|
||||||
|
'one_time_turnover' => 'yes',
|
||||||
|
'multiplier' => $order->multiplier,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function markFailedAttempt(MallOrder $order, string $reason): void
|
||||||
|
{
|
||||||
|
// retry_count 在“准备发送”阶段已 +1;此处用当前 retry_count 作为 attempt 编号
|
||||||
|
$retryCount = $order->retry_count ?? 0;
|
||||||
|
$attempt = is_int($retryCount) ? $retryCount : (is_numeric($retryCount) ? intval($retryCount) : 0);
|
||||||
|
if ($attempt <= 0) {
|
||||||
|
$attempt = 1;
|
||||||
|
$order->retry_count = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$prev = $order->fail_reason;
|
||||||
|
$prefix = 'attempt ' . $attempt . ': ';
|
||||||
|
$line = $prefix . $reason;
|
||||||
|
$newReason = $line;
|
||||||
|
if (is_string($prev) && $prev !== '') {
|
||||||
|
$newReason = $prev . "\n" . $line;
|
||||||
|
}
|
||||||
|
|
||||||
|
$final = $attempt >= self::MAX_RETRY;
|
||||||
|
$order->grant_status = $final ? MallOrder::GRANT_FAILED_FINAL : MallOrder::GRANT_FAILED_RETRYABLE;
|
||||||
|
$order->fail_reason = $newReason;
|
||||||
|
$order->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成对方要求的 Base64(HMAC-SHA1)
|
||||||
|
* - 文档中示例 python 会 base64_decode(key) 后参与 hmac
|
||||||
|
* - 生产 key 由 BA 提供,可能是 base64 或 hex;这里做兼容处理
|
||||||
|
*/
|
||||||
|
private function buildSignature(string $input, string $authKey): ?string
|
||||||
|
{
|
||||||
|
$keyBytes = null;
|
||||||
|
|
||||||
|
$maybeBase64 = base64_decode($authKey, true);
|
||||||
|
if ($maybeBase64 !== false && $maybeBase64 !== '') {
|
||||||
|
$keyBytes = $maybeBase64;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($keyBytes === null) {
|
||||||
|
$isHex = ctype_xdigit($authKey) && (strlen($authKey) % 2 === 0);
|
||||||
|
if ($isHex) {
|
||||||
|
$hex = hex2bin($authKey);
|
||||||
|
if ($hex !== false && $hex !== '') {
|
||||||
|
$keyBytes = $hex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($keyBytes === null) {
|
||||||
|
$keyBytes = $authKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = hash_hmac('sha1', $input, $keyBytes, true);
|
||||||
|
if (!is_string($raw) || $raw === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64_encode($raw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -40,7 +40,8 @@
|
|||||||
"nelexa/zip": "^4.0.0",
|
"nelexa/zip": "^4.0.0",
|
||||||
"voku/anti-xss": "^4.1",
|
"voku/anti-xss": "^4.1",
|
||||||
"topthink/think-validate": "^3.0",
|
"topthink/think-validate": "^3.0",
|
||||||
"firebase/php-jwt": "^7.0"
|
"firebase/php-jwt": "^7.0",
|
||||||
|
"guzzlehttp/guzzle": "^7.10"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
"ext-event": "For better performance. "
|
"ext-event": "For better performance. "
|
||||||
|
|||||||
@@ -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', '')),
|
||||||
@@ -33,4 +33,22 @@ return [
|
|||||||
'balance_credit_url' => '/api/v1/balance/credit',
|
'balance_credit_url' => '/api/v1/balance/credit',
|
||||||
'transaction_status_url' => '/api/v1/transaction/status',
|
'transaction_status_url' => '/api/v1/transaction/status',
|
||||||
],
|
],
|
||||||
|
// Angpow Import API(商城调用对方 cashmarket/merchant-api 时使用)
|
||||||
|
'angpow_import' => [
|
||||||
|
// 对方 base_url,例如 https://ss2-staging2.ttwd8.com
|
||||||
|
'base_url' => strval(env('PLAYX_ANGPOW_IMPORT_BASE_URL', '')),
|
||||||
|
// 路径:文档示例为 /api/v3/merchant/angpow-imports;对方 curl 示例为 /cashmarket/v3/merchant-api/angpow-imports
|
||||||
|
'path' => strval(env('PLAYX_ANGPOW_IMPORT_PATH', '/api/v3/merchant/angpow-imports')),
|
||||||
|
// merchant_code(固定 plx 或按环境配置)
|
||||||
|
'merchant_code' => strval(env('PLAYX_ANGPOW_MERCHANT_CODE', 'plx')),
|
||||||
|
// HMAC-SHA1 的 auth key(生产环境由 BA 提供)
|
||||||
|
'auth_key' => strval(env('PLAYX_ANGPOW_IMPORT_AUTH_KEY', '')),
|
||||||
|
// HTTPS:指定 CA 证书包路径(推荐,下载 https://curl.se/ca/cacert.pem 后填绝对路径,可避免 cURL error 60)
|
||||||
|
'ca_file' => strval(env('PLAYX_ANGPOW_IMPORT_CACERT', '')),
|
||||||
|
// 是否校验 SSL(生产必须为 true;本地无 CA 时可临时 false,勿用于生产)
|
||||||
|
'verify_ssl' => filter_var(env('PLAYX_ANGPOW_IMPORT_VERIFY_SSL', '1'), FILTER_VALIDATE_BOOLEAN),
|
||||||
|
// 固定货币展示映射
|
||||||
|
'currency' => 'MYR',
|
||||||
|
'visual_name' => 'Angpow',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
use support\Log;
|
use support\Log;
|
||||||
use support\Request;
|
use support\Request;
|
||||||
use app\process\Http;
|
use app\process\Http;
|
||||||
|
use app\process\AngpowImportJobs;
|
||||||
|
|
||||||
global $argv;
|
global $argv;
|
||||||
|
|
||||||
@@ -65,4 +66,9 @@ return [
|
|||||||
'handler' => app\process\PlayxJobs::class,
|
'handler' => app\process\PlayxJobs::class,
|
||||||
'reloadable' => false,
|
'reloadable' => false,
|
||||||
],
|
],
|
||||||
|
// Angpow 导入推送任务:订单兑换后推送到对方平台
|
||||||
|
'angpow_import_jobs' => [
|
||||||
|
'handler' => AngpowImportJobs::class,
|
||||||
|
'reloadable' => false,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ Route::post('/api/v1/mall/bonusRedeem', [\app\api\controller\v1\Playx::class, 'b
|
|||||||
Route::post('/api/v1/mall/physicalRedeem', [\app\api\controller\v1\Playx::class, 'physicalRedeem']);
|
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::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/orders', [\app\api\controller\v1\Playx::class, 'orders']);
|
||||||
|
Route::get('/api/v1/mall/pointsLogs', [\app\api\controller\v1\Playx::class, 'pointsLogs']);
|
||||||
Route::get('/api/v1/mall/addressList', [\app\api\controller\v1\Playx::class, 'addressList']);
|
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/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/addressEdit', [\app\api\controller\v1\Playx::class, 'addressEdit']);
|
||||||
|
|||||||
@@ -159,6 +159,47 @@ curl "https://{域名}/api/v1/temLogin?username=test001"
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 3.9 积分流水(领取/兑换/退回)
|
||||||
|
|
||||||
|
**GET** ` /api/v1/mall/pointsLogs `
|
||||||
|
|
||||||
|
鉴权:携带 `token` 或 `session_id` 或 `user_id`
|
||||||
|
|
||||||
|
参数(Query):
|
||||||
|
- `limit`:可选,每页条数(默认 20,最大 100)
|
||||||
|
- `cursor`:可选,游标(上一页响应返回的 `next_cursor`,传入后获取下一页)
|
||||||
|
- `direction`:可选,`IN`(入账)/`OUT`(扣减),不传返回全部
|
||||||
|
|
||||||
|
返回(成功 `data`):
|
||||||
|
- `list[]`:流水数组(按时间倒序)
|
||||||
|
- `biz_type`:`CLAIM`(领取入账)/ `REDEEM_BONUS|REDEEM_PHYSICAL|REDEEM_WITHDRAW`(兑换扣分)/ `REFUND`(订单驳回或终态失败退回)
|
||||||
|
- `direction`:`IN` / `OUT`
|
||||||
|
- `points`:积分变动值(正数,方向由 `direction` 表示)
|
||||||
|
- `ts`:时间戳(Unix 秒)
|
||||||
|
- `ref_id`:关联业务号(领取为 `claim_request_id`;订单为 `external_transaction_id`)
|
||||||
|
- `order_no`:订单号(非订单类为空)
|
||||||
|
- `order_status`:订单状态(非订单类为空)
|
||||||
|
- `mallItem`:商品信息(非订单类为 null)
|
||||||
|
- `id`:商品ID
|
||||||
|
- `title`:商品名
|
||||||
|
- `type`:商品类型(`1=BONUS 2=PHYSICAL 3=WITHDRAW`)
|
||||||
|
- `score`:所需积分
|
||||||
|
- `amount`:现金面值(红利/提现档位)
|
||||||
|
- `multiplier`:流水倍数
|
||||||
|
- `category`:红利业务类别
|
||||||
|
- `category_title`:类别展示名
|
||||||
|
- `item_id/item_title/item_type/item_score`:兼容字段(建议优先使用 `mallItem`)
|
||||||
|
- `cursor`:当前记录游标(用于分页)
|
||||||
|
- `next_cursor`:下一页游标(为本页最后一条的 `cursor`;若本页为空则为 null)
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -G "https://{域名}/api/v1/mall/pointsLogs" \
|
||||||
|
-H "token: <muser_token>" \
|
||||||
|
--data-urlencode "limit=20"
|
||||||
|
```
|
||||||
|
|
||||||
## 4. 地址管理(H5)
|
## 4. 地址管理(H5)
|
||||||
|
|
||||||
> 地址与资产主体通过 `playx_user_asset_id` 关联(即 `mall_user_asset.id`)。
|
> 地址与资产主体通过 `playx_user_asset_id` 关联(即 `mall_user_asset.id`)。
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ flowchart LR
|
|||||||
|
|
||||||
### 4.1 登录鉴权(Iframe + token)
|
### 4.1 登录鉴权(Iframe + token)
|
||||||
|
|
||||||
> **接口与字段细节**以代码为准,完整说明见同目录《PlayX-接口文档.md》(§3 H5、§3.2 `temLogin`、§3.3 `verify-token`)。
|
> **接口与字段细节**以代码为准,完整说明见同目录《PlayX-接口文档.md》(§3 H5、§3.2 `temLogin`、§3.3 `verifyToken`)。
|
||||||
|
|
||||||
#### 4.1.1 身份与数据模型(商城侧)
|
#### 4.1.1 身份与数据模型(商城侧)
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ flowchart LR
|
|||||||
|
|
||||||
1. 用户在 PlayX 内打开积分商城入口(iframe)。
|
1. 用户在 PlayX 内打开积分商城入口(iframe)。
|
||||||
2. PlayX 前端通过 postMessage 将 **PlayX 下发的 token**(及必要上下文)传给商城 H5。
|
2. PlayX 前端通过 postMessage 将 **PlayX 下发的 token**(及必要上下文)传给商城 H5。
|
||||||
3. 商城 H5 调用商城后端 **`POST /api/v1/playx/verify-token`**,由商城向 PlayX 的 **Token Verification API**(`playx.api.base_url` + `playx.api.token_verify_url`)发起校验。
|
3. 商城 H5 调用商城后端 **`POST /api/v1/mall/verifyToken`**,由商城向 PlayX 的 **Token Verification API**(`playx.api.base_url` + `playx.api.token_verify_url`)发起校验。
|
||||||
4. **前提**:配置 **`playx.verify_token_local_only = false`**,且 **`playx.api.base_url`** 已配置为可访问的 PlayX 基地址。
|
4. **前提**:配置 **`playx.verify_token_local_only = false`**,且 **`playx.api.base_url`** 已配置为可访问的 PlayX 基地址。
|
||||||
5. PlayX 返回 **`user_id`、`username`**(及可选会话过期时间等)。
|
5. PlayX 返回 **`user_id`、`username`**(及可选会话过期时间等)。
|
||||||
6. 商城写入 **`mall_playx_session`**(`session_id` + 上述 `user_id`/`username` + 过期时间),后续 H5 可用 **`session_id`** 或 **`token`(商城临时 token,见模式 B)** 调用资产/领取等接口。
|
6. 商城写入 **`mall_playx_session`**(`session_id` + 上述 `user_id`/`username` + 过期时间),后续 H5 可用 **`session_id`** 或 **`token`(商城临时 token,见模式 B)** 调用资产/领取等接口。
|
||||||
@@ -86,9 +86,9 @@ flowchart LR
|
|||||||
用于开发、联调前自测、或 PlayX 接口未就绪时:
|
用于开发、联调前自测、或 PlayX 接口未就绪时:
|
||||||
|
|
||||||
1. 配置 **`playx.verify_token_local_only = true`**(环境变量 **`PLAYX_VERIFY_TOKEN_LOCAL_ONLY`**,默认可为开启,以项目 `config/playx.php` 为准)。
|
1. 配置 **`playx.verify_token_local_only = true`**(环境变量 **`PLAYX_VERIFY_TOKEN_LOCAL_ONLY`**,默认可为开启,以项目 `config/playx.php` 为准)。
|
||||||
2. 此时 **`/api/v1/playx/verify-token` 不会访问 PlayX**,仅在商城内校验 **商城临时 token**(token 表类型 **`muser`**,由下方 `temLogin` 签发)。
|
2. 此时 **`/api/v1/mall/verifyToken` 不会访问 PlayX**,仅在商城内校验 **商城临时 token**(token 表类型 **`muser`**,由下方 `temLogin` 签发)。
|
||||||
3. 调用 **`GET/POST /api/v1/temLogin?username=...`**(需 **`buildadmin.agent_auth.temp_login_enable = true`**):不存在则创建 **`mall_user`**,并保证存在 **`mall_playx_user_asset`**(含 `playx_user_id`,默认 **`mall_{id}`**),返回 **`userInfo.token`**、**`playx_user_id`**、**`expires_in`** 等。
|
3. 调用 **`GET/POST /api/v1/temLogin?username=...`**(需 **`buildadmin.agent_auth.temp_login_enable = true`**):不存在则创建 **`mall_user`**,并保证存在 **`mall_playx_user_asset`**(含 `playx_user_id`,默认 **`mall_{id}`**),返回 **`userInfo.token`**、**`playx_user_id`**、**`expires_in`** 等。
|
||||||
4. 再用该 token 调用 **`verify-token`** 可得到 **`session_id`**,与模式 A 一样供后续接口使用;或直接带 **`token` / `ba-token`** 调资产等接口(见《PlayX-接口文档》§3.1)。
|
4. 再用该 token 调用 **`verifyToken`** 可得到 **`session_id`**,与模式 A 一样供后续接口使用;或直接带 **`token` / `ba-token`** 调资产等接口(见《PlayX-接口文档》§3.1)。
|
||||||
|
|
||||||
#### 4.1.4 会话续期与前端约定
|
#### 4.1.4 会话续期与前端约定
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ flowchart LR
|
|||||||
|
|
||||||
- **目的**:推送昨日玩家数据,用于 T+1 计算入池与领取上限。
|
- **目的**:推送昨日玩家数据,用于 T+1 计算入池与领取上限。
|
||||||
- **幂等键**:`user_id + date`(date 建议为 PlayX 业务日)
|
- **幂等键**:`user_id + date`(date 建议为 PlayX 业务日)
|
||||||
- **Method/Path(建议)**:`POST /api/v1/playx/daily-push`
|
- **Method/Path(建议)**:`POST /api/v1/mall/dailyPush`
|
||||||
|
|
||||||
请求字段说明(最小集合,来自现有资料):
|
请求字段说明(最小集合,来自现有资料):
|
||||||
|
|
||||||
@@ -431,7 +431,7 @@ flowchart LR
|
|||||||
|
|
||||||
其中:
|
其中:
|
||||||
|
|
||||||
- `PATH` 不含域名与 querystring(例如 `/api/v1/playx/daily-push`)。
|
- `PATH` 不含域名与 querystring(例如 `/api/v1/mall/dailyPush`)。
|
||||||
- `REQUEST_BODY_JSON` 使用原始 request body(不做 key 排序时,需双方约定序列化方式;更推荐双方统一为“key 排序后的紧凑 JSON”)。
|
- `REQUEST_BODY_JSON` 使用原始 request body(不做 key 排序时,需双方约定序列化方式;更推荐双方统一为“key 排序后的紧凑 JSON”)。
|
||||||
|
|
||||||
## 8. 错误码与可观测(建议)
|
## 8. 错误码与可观测(建议)
|
||||||
|
|||||||
@@ -823,6 +823,64 @@ curl -G 'http://localhost:1818/api/v1/mall/orders' --data-urlencode 'token=上
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.11 同步额度(可选)
|
### 3.11 积分流水(Points Logs)
|
||||||
|
* 方法:`GET`
|
||||||
|
* 路径:`/api/v1/mall/pointsLogs`
|
||||||
|
|
||||||
|
#### 请求参数(鉴权)
|
||||||
|
同 **3.1**(`session_id` / `token` / `user_id`)。
|
||||||
|
|
||||||
|
#### Query 参数
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `limit` | int | 否 | 每页条数,默认 20,最大 100 |
|
||||||
|
| `cursor` | string | 否 | 游标(上一页返回 `next_cursor`) |
|
||||||
|
| `direction` | string | 否 | `IN` 仅入账 / `OUT` 仅扣减;不传返回全部 |
|
||||||
|
|
||||||
|
#### 返回(成功 data)
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `data.list` | array | 流水数组(时间倒序) |
|
||||||
|
| `data.next_cursor` | string\|null | 下一页游标(本页最后一条记录的游标) |
|
||||||
|
|
||||||
|
`list` 每一项字段:
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `biz_type` | string | `CLAIM`/`REDEEM_BONUS`/`REDEEM_PHYSICAL`/`REDEEM_WITHDRAW`/`REFUND` |
|
||||||
|
| `direction` | string | `IN`/`OUT` |
|
||||||
|
| `points` | int | 积分变动值(正数,方向由 `direction` 表示) |
|
||||||
|
| `ts` | int | Unix 秒 |
|
||||||
|
| `ref_id` | string | 领取为 `claim_request_id`;订单为 `external_transaction_id` |
|
||||||
|
| `order_no` | string | 订单号(非订单类为空) |
|
||||||
|
| `order_status` | string | 订单状态(非订单类为空) |
|
||||||
|
| `mallItem` | object\|null | 商品信息(非订单类为 null) |
|
||||||
|
| `item_id` | int | 兼容字段:商品ID(非订单类为 0) |
|
||||||
|
| `item_title` | string | 兼容字段:商品标题(非订单类为空) |
|
||||||
|
| `item_type` | int | 兼容字段:商品类型(非订单类为 0;`1=BONUS 2=PHYSICAL 3=WITHDRAW`) |
|
||||||
|
| `item_score` | int | 兼容字段:商品所需积分(非订单类为 0) |
|
||||||
|
| `cursor` | string | 当前记录游标 |
|
||||||
|
|
||||||
|
`mallItem` 字段(非隐私):
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | int | `mall_item.id` |
|
||||||
|
| `title` | string | 商品名 |
|
||||||
|
| `type` | int | 商品类型 |
|
||||||
|
| `score` | int | 所需积分 |
|
||||||
|
| `amount` | number | 现金面值 |
|
||||||
|
| `multiplier` | int | 流水倍数 |
|
||||||
|
| `category` | string | 红利业务类别 |
|
||||||
|
| `category_title` | string | 类别展示名 |
|
||||||
|
|
||||||
|
#### 示例
|
||||||
|
```bash
|
||||||
|
curl -G 'http://localhost:1818/api/v1/mall/pointsLogs' \
|
||||||
|
--data-urlencode 'token=上一步temLogin返回的token' \
|
||||||
|
--data-urlencode 'limit=20'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.12 同步额度(可选)
|
||||||
当前代码未实现并未注册路由:`/api/v1/mall/syncLimit`。
|
当前代码未实现并未注册路由:`/api/v1/mall/syncLimit`。
|
||||||
|
|
||||||
|
|||||||
BIN
docs/import-angpow-api.pdf
Normal file
BIN
docs/import-angpow-api.pdf
Normal file
Binary file not shown.
@@ -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 用户资产与人工调账
|
||||||
|
|||||||
@@ -270,7 +270,7 @@
|
|||||||
|
|
||||||
### 6.2 发放请求状态机(商城内部)
|
### 6.2 发放请求状态机(商城内部)
|
||||||
|
|
||||||
针对每个订单(BONUS/WITHDRAW),在商城内部维护发放子状态,例如:
|
针对每个订单(BONUS/WITHDRAW),在商城内部维护推送playx状态,例如:
|
||||||
|
|
||||||
- `NOT_SENT`:未发送给 PlayX。
|
- `NOT_SENT`:未发送给 PlayX。
|
||||||
- `SENT_PENDING`:已发送,等待 PlayX 响应。
|
- `SENT_PENDING`:已发送,等待 PlayX 响应。
|
||||||
@@ -282,7 +282,7 @@
|
|||||||
|
|
||||||
- 只有在 `NOT_SENT` 或 `FAILED_RETRYABLE` 状态下才允许“再次发送”。
|
- 只有在 `NOT_SENT` 或 `FAILED_RETRYABLE` 状态下才允许“再次发送”。
|
||||||
- 一旦进入 `ACCEPTED`,不得再发请求(自动或人工)。
|
- 一旦进入 `ACCEPTED`,不得再发请求(自动或人工)。
|
||||||
- 订单业务状态(PENDING/COMPLETED/REJECTED)与发放子状态之间要有清晰映射:
|
- 订单业务状态(PENDING/COMPLETED/REJECTED)与推送playx状态之间要有清晰映射:
|
||||||
- `ACCEPTED` + 对账确认成功 → `COMPLETED`。
|
- `ACCEPTED` + 对账确认成功 → `COMPLETED`。
|
||||||
- `FAILED_FINAL` → `REJECTED`(并退积分)。
|
- `FAILED_FINAL` → `REJECTED`(并退积分)。
|
||||||
|
|
||||||
|
|||||||
11
tmp_sig.php
11
tmp_sig.php
@@ -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";
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
119
web/src/lang/backend/en/menu.ts
Normal file
119
web/src/lang/backend/en/menu.ts
Normal 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',
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -2,7 +2,10 @@
|
|||||||
* 后台公共语言包
|
* 后台公共语言包
|
||||||
* 覆盖风险:请避免使用页面语言包的目录名、文件名作为翻译 key
|
* 覆盖风险:请避免使用页面语言包的目录名、文件名作为翻译 key
|
||||||
*/
|
*/
|
||||||
|
import menu from './zh-cn/menu'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
menu,
|
||||||
Balance: '余额',
|
Balance: '余额',
|
||||||
Integral: '积分',
|
Integral: '积分',
|
||||||
Connection: '连接标识',
|
Connection: '连接标识',
|
||||||
|
|||||||
@@ -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',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
title: 'PlayX 对接中心',
|
title: 'PlayX 对接中心',
|
||||||
desc: '集中管理积分商城与 PlayX 的订单、推送、领取与资产数据。建议优先处理“发放子状态=失败可重试”的订单。',
|
desc: '集中管理积分商城与 PlayX 的订单、推送、领取与资产数据。建议优先处理“推送playx状态=失败可重试”的订单。',
|
||||||
orders: '统一订单',
|
orders: '统一订单',
|
||||||
dailyPush: '每日推送',
|
dailyPush: '每日推送',
|
||||||
claimLog: '领取记录',
|
claimLog: '领取记录',
|
||||||
|
|||||||
@@ -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: '物流公司',
|
||||||
|
|||||||
120
web/src/lang/backend/zh-cn/menu.ts
Normal file
120
web/src/lang/backend/zh-cn/menu.ts
Normal 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: '兑换订单',
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
33
web/src/utils/menuI18n.ts
Normal 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'))
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user