diff --git a/app/admin/controller/Dashboard.php b/app/admin/controller/Dashboard.php index 4843c56..9cd6773 100644 --- a/app/admin/controller/Dashboard.php +++ b/app/admin/controller/Dashboard.php @@ -66,7 +66,7 @@ class Dashboard extends Backend $pendingPhysicalToShip = MallOrder::where('type', MallOrder::TYPE_PHYSICAL) ->where('status', MallOrder::STATUS_PENDING) ->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) ->count(); diff --git a/app/admin/controller/mall/Order.php b/app/admin/controller/mall/Order.php index 6d6cd6d..c926c4a 100644 --- a/app/admin/controller/mall/Order.php +++ b/app/admin/controller/mall/Order.php @@ -202,7 +202,7 @@ class Order extends Backend $id = $data['id'] ?? 0; $rejectReason = $data['reject_reason'] ?? ''; - if (!$id || $rejectReason === '') { + if (!$id) { return $this->error(__('Missing required fields')); } @@ -214,6 +214,10 @@ class Order extends Backend return $this->error(__('Order status must be PENDING')); } + if ($order->type === MallOrder::TYPE_PHYSICAL && $rejectReason === '') { + return $this->error(__('Missing required fields')); + } + Db::startTrans(); try { $asset = MallUserAsset::where('playx_user_id', $order->user_id ?? '')->find(); @@ -229,7 +233,11 @@ class Order extends Backend $order->status = MallOrder::STATUS_REJECTED; $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->save(); @@ -243,7 +251,7 @@ class Order extends Backend } /** - * 手动重试(仅红利/提现,且必须 FAILED_RETRYABLE) + * 手动重试(仅红利推送失败可重试) */ public function retry(Request $request): Response { @@ -265,8 +273,8 @@ class Order extends Backend if (!$order) { return $this->error(__('Record not found')); } - if (!in_array($order->type, [MallOrder::TYPE_BONUS, MallOrder::TYPE_WITHDRAW], true)) { - return $this->error(__('Only BONUS/WITHDRAW can retry')); + if ($order->type !== MallOrder::TYPE_BONUS) { + return $this->error(__('Only BONUS can retry')); } if ($order->grant_status !== MallOrder::GRANT_FAILED_RETRYABLE) { return $this->error(__('Only FAILED_RETRYABLE can retry')); diff --git a/app/api/controller/v1/Playx.php b/app/api/controller/v1/Playx.php index 12b076c..47f1563 100644 --- a/app/api/controller/v1/Playx.php +++ b/app/api/controller/v1/Playx.php @@ -1016,6 +1016,7 @@ class Playx extends Api 'receiver_name' => $snapshot['receiver_name'], 'receiver_phone' => $snapshot['receiver_phone'], 'receiver_address' => $snapshot['receiver_address'], + 'grant_status' => MallOrder::GRANT_NOT_APPLICABLE, 'create_time' => time(), 'update_time' => time(), ]); @@ -1082,7 +1083,7 @@ class Playx extends Api 'amount' => $amount, 'multiplier' => $multiplier, 'external_transaction_id' => $orderNo, - 'grant_status' => MallOrder::GRANT_NOT_SENT, + 'grant_status' => MallOrder::GRANT_NOT_APPLICABLE, 'create_time' => time(), 'update_time' => time(), ]); @@ -1093,11 +1094,6 @@ class Playx extends Api 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'), [ 'order_id' => $order->id, 'status' => 'PENDING', @@ -1143,39 +1139,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(); - } - } } diff --git a/app/common/model/MallOrder.php b/app/common/model/MallOrder.php index 3e2ebb8..5f3b521 100644 --- a/app/common/model/MallOrder.php +++ b/app/common/model/MallOrder.php @@ -51,6 +51,9 @@ class MallOrder extends Model public const GRANT_FAILED_RETRYABLE = 'FAILED_RETRYABLE'; public const GRANT_FAILED_FINAL = 'FAILED_FINAL'; + /** 非红利订单不参与 PlayX/Angpow 推送,固定为该占位值 */ + public const GRANT_NOT_APPLICABLE = '---'; + protected array $type = [ 'create_time' => 'integer', 'update_time' => 'integer', diff --git a/app/process/PlayxJobs.php b/app/process/PlayxJobs.php index 975e546..db4fcb2 100644 --- a/app/process/PlayxJobs.php +++ b/app/process/PlayxJobs.php @@ -47,7 +47,8 @@ class PlayxJobs $path = strval(config('playx.api.transaction_status_url', '/api/v1/transaction/status')); $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) ->order('id', 'desc') ->limit(50) @@ -99,12 +100,11 @@ class PlayxJobs } $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; - $list = MallOrder::whereIn('grant_status', [ + $list = MallOrder::where('type', MallOrder::TYPE_BONUS) + ->whereIn('grant_status', [ MallOrder::GRANT_NOT_SENT, MallOrder::GRANT_FAILED_RETRYABLE, ]) @@ -124,7 +124,7 @@ class PlayxJobs $order->retry_count = intval($order->retry_count ?? 0) + 1; try { - $this->sendGrantByOrder($order, $bonusUrl, $withdrawUrl, $maxRetry); + $this->sendGrantByOrder($order, $bonusUrl, $maxRetry); } catch (\Throwable $e) { $order->fail_reason = $e->getMessage(); if (intval($order->retry_count) >= $maxRetry) { @@ -167,7 +167,7 @@ class PlayxJobs return false; } - private function sendGrantByOrder(MallOrder $order, string $bonusUrl, string $withdrawUrl, int $maxRetry): void + private function sendGrantByOrder(MallOrder $order, string $bonusUrl, int $maxRetry): void { $item = null; if ($order->mallItem) { @@ -221,45 +221,7 @@ class PlayxJobs return; } - if ($order->type === MallOrder::TYPE_WITHDRAW) { - $multiplier = intval($order->multiplier ?? 0); - if ($multiplier <= 0) { - $multiplier = 1; - } - $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(); - return; - } - - // PHYSICAL 目前由后台手工发货/驳回,不参与 PlayX 发放重试 + // 非 BONUS 订单不参与 PlayX 发放重试(提现/实物由后台流程处理) } private function refundPoints(MallOrder $order): void diff --git a/docs/积分商城-PlayX对接实施方案.md b/docs/积分商城-PlayX对接实施方案.md index 693071b..be42de9 100644 --- a/docs/积分商城-PlayX对接实施方案.md +++ b/docs/积分商城-PlayX对接实施方案.md @@ -526,7 +526,7 @@ curl -G 'http://localhost:1818/api/v1/mall/orders' --data-urlencode 'session_id= - 发货:录入物流公司、单号 → `SHIPPED` - 驳回:录入驳回原因 → `REJECTED`,自动退回积分 - **红利/提现订单**: - - 展示 `external_transaction_id`、`playx_transaction_id`、发放子状态 + - 展示 `external_transaction_id`、`playx_transaction_id`、推送playx - 手动重试:仅对 `FAILED_RETRYABLE` 状态,记录 `retry_request_id`、操作者、原因 ### 2.3 用户资产与人工调账 diff --git a/web/src/lang/backend/en/mall/order.ts b/web/src/lang/backend/en/mall/order.ts index 5661cab..18845ef 100644 --- a/web/src/lang/backend/en/mall/order.ts +++ b/web/src/lang/backend/en/mall/order.ts @@ -23,6 +23,7 @@ export default { 'grant_status ACCEPTED': 'ACCEPTED', 'grant_status FAILED_RETRYABLE': 'FAILED_RETRYABLE', 'grant_status FAILED_FINAL': 'FAILED_FINAL', + 'grant_status ---': '---', fail_reason: 'fail_reason', reject_reason: 'reject_reason', shipping_company: 'shipping_company', diff --git a/web/src/lang/backend/en/mall/playxOrder.ts b/web/src/lang/backend/en/mall/playxOrder.ts index ff648ee..9ede71e 100644 --- a/web/src/lang/backend/en/mall/playxOrder.ts +++ b/web/src/lang/backend/en/mall/playxOrder.ts @@ -23,6 +23,7 @@ export default { 'grant_status ACCEPTED': 'ACCEPTED', 'grant_status FAILED_RETRYABLE': 'FAILED_RETRYABLE', 'grant_status FAILED_FINAL': 'FAILED_FINAL', + 'grant_status ---': '---', fail_reason: 'fail_reason', reject_reason: 'reject_reason', shipping_company: 'shipping_company', diff --git a/web/src/lang/backend/zh-cn/mall/order.ts b/web/src/lang/backend/zh-cn/mall/order.ts index c771e60..badffef 100644 --- a/web/src/lang/backend/zh-cn/mall/order.ts +++ b/web/src/lang/backend/zh-cn/mall/order.ts @@ -17,12 +17,13 @@ export default { multiplier: '流水倍数', external_transaction_id: '订单号', playx_transaction_id: 'PlayX流水号', - grant_status: '发放子状态', + grant_status: '推送playx状态', 'grant_status NOT_SENT': '未发送', 'grant_status SENT_PENDING': '已发送排队', 'grant_status ACCEPTED': '已接收(accepted)', 'grant_status FAILED_RETRYABLE': '失败可重试', 'grant_status FAILED_FINAL': '失败最终', + 'grant_status ---': '---', fail_reason: '失败原因', reject_reason: '驳回原因', shipping_company: '物流公司', diff --git a/web/src/lang/backend/zh-cn/mall/playxCenter.ts b/web/src/lang/backend/zh-cn/mall/playxCenter.ts index 822f813..0f61436 100644 --- a/web/src/lang/backend/zh-cn/mall/playxCenter.ts +++ b/web/src/lang/backend/zh-cn/mall/playxCenter.ts @@ -1,6 +1,6 @@ export default { title: 'PlayX 对接中心', - desc: '集中管理积分商城与 PlayX 的订单、推送、领取与资产数据。建议优先处理“发放子状态=失败可重试”的订单。', + desc: '集中管理积分商城与 PlayX 的订单、推送、领取与资产数据。建议优先处理“推送playx状态=失败可重试”的订单。', orders: '统一订单', dailyPush: '每日推送', claimLog: '领取记录', diff --git a/web/src/lang/backend/zh-cn/mall/playxOrder.ts b/web/src/lang/backend/zh-cn/mall/playxOrder.ts index b4bfef3..187f240 100644 --- a/web/src/lang/backend/zh-cn/mall/playxOrder.ts +++ b/web/src/lang/backend/zh-cn/mall/playxOrder.ts @@ -17,12 +17,13 @@ export default { multiplier: '流水倍数', external_transaction_id: '订单号', playx_transaction_id: 'PlayX流水号', - grant_status: '发放子状态', + grant_status: '推送playx状态', 'grant_status NOT_SENT': '未发送', 'grant_status SENT_PENDING': '已发送排队', 'grant_status ACCEPTED': '已接收(accepted)', 'grant_status FAILED_RETRYABLE': '失败可重试', 'grant_status FAILED_FINAL': '失败最终', + 'grant_status ---': '---', fail_reason: '失败原因', reject_reason: '驳回原因', shipping_company: '物流公司', diff --git a/web/src/views/backend/mall/order/index.vue b/web/src/views/backend/mall/order/index.vue index 8dcec94..dc58ea6 100644 --- a/web/src/views/backend/mall/order/index.vue +++ b/web/src/views/backend/mall/order/index.vue @@ -125,6 +125,15 @@ const baTable = new baTableClass( label: t('mall.order.grant_status'), prop: 'grant_status', align: 'center', + minWidth: 100, + custom: { + NOT_SENT: 'info', + SENT_PENDING: 'primary', + ACCEPTED: 'primary', + FAILED_RETRYABLE: 'error', + FAILED_FINAL: 'error', + '---': 'info', + }, operator: 'eq', sortable: false, render: 'tag', @@ -134,6 +143,7 @@ const baTable = new baTableClass( ACCEPTED: t('mall.order.grant_status ACCEPTED'), FAILED_RETRYABLE: t('mall.order.grant_status FAILED_RETRYABLE'), FAILED_FINAL: t('mall.order.grant_status FAILED_FINAL'), + '---': t('mall.order.grant_status ---'), }, }, { @@ -237,8 +247,7 @@ const baTable = new baTableClass( text: '手动重试', type: 'warning', icon: '', - display: (row: TableRow) => - (row.type === 'BONUS' || row.type === 'WITHDRAW') && row.grant_status === 'FAILED_RETRYABLE' && row.status === 'PENDING', + display: (row: TableRow) => row.type === 'BONUS' && row.grant_status === 'FAILED_RETRYABLE' && row.status === 'PENDING', popconfirm: { title: '确认将该订单加入重试队列?', confirmButtonText: '确认', diff --git a/web/src/views/backend/mall/order/popupForm.vue b/web/src/views/backend/mall/order/popupForm.vue index 1d09d49..87b057e 100644 --- a/web/src/views/backend/mall/order/popupForm.vue +++ b/web/src/views/backend/mall/order/popupForm.vue @@ -57,7 +57,7 @@ diff --git a/web/src/views/backend/mall/playxOrder/index.vue b/web/src/views/backend/mall/playxOrder/index.vue index f75db2c..8336b53 100644 --- a/web/src/views/backend/mall/playxOrder/index.vue +++ b/web/src/views/backend/mall/playxOrder/index.vue @@ -83,6 +83,7 @@ const baTable = new baTableClass( ACCEPTED: t('mall.playxOrder.grant_status ACCEPTED'), FAILED_RETRYABLE: t('mall.playxOrder.grant_status FAILED_RETRYABLE'), FAILED_FINAL: t('mall.playxOrder.grant_status FAILED_FINAL'), + '---': t('mall.playxOrder.grant_status ---'), }, }, { @@ -115,8 +116,7 @@ const baTable = new baTableClass( text: '手动重试', type: 'warning', icon: '', - display: (row: TableRow) => - (row.type === 'BONUS' || row.type === 'WITHDRAW') && row.grant_status === 'FAILED_RETRYABLE' && row.status === 'PENDING', + display: (row: TableRow) => row.type === 'BONUS' && row.grant_status === 'FAILED_RETRYABLE' && row.status === 'PENDING', popconfirm: { title: '确认将该订单加入重试队列?', confirmButtonText: '确认',