From 1c900e7132506bee1d28b4d35ad1ac91ced13c2d Mon Sep 17 00:00:00 2001 From: zhenhui <1276357500@qq.com> Date: Tue, 14 Apr 2026 18:55:31 +0800 Subject: [PATCH] =?UTF-8?q?1.=E4=BF=AE=E5=A4=8Dindex.vue=E8=A1=A8=E6=A0=BC?= =?UTF-8?q?=E7=BF=BB=E8=AF=91=E9=94=99=E8=AF=AF=202.=E9=99=90=E5=88=B6?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E8=AE=A2=E5=8D=95=E7=B1=BB=E5=9E=8B=E5=AE=A1?= =?UTF-8?q?=E6=A0=B8=203.=E7=BB=9F=E4=B8=80=E8=AE=A2=E5=8D=95=E6=89=8B?= =?UTF-8?q?=E5=8A=A8=E6=8E=A8=E9=80=81=E6=8A=A5=E9=94=99PlayX=20=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E6=9C=AA=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/controller/mall/Order.php | 7 +- app/common/library/MallBonusGrantPush.php | 131 +++++++++++++++--- app/process/AngpowImportJobs.php | 4 +- web/src/lang/autoload.ts | 4 + web/src/lang/backend/en/mall/item.ts | 45 +++--- web/src/views/backend/mall/order/index.vue | 1 + .../views/backend/mall/order/popupForm.vue | 7 +- 7 files changed, 150 insertions(+), 49 deletions(-) diff --git a/app/admin/controller/mall/Order.php b/app/admin/controller/mall/Order.php index 5791bc7..07cd4c9 100644 --- a/app/admin/controller/mall/Order.php +++ b/app/admin/controller/mall/Order.php @@ -166,7 +166,7 @@ class Order extends Backend if ($order->status !== MallOrder::STATUS_PENDING) { return $this->error(__('Order status must be PENDING')); } - if ($order->type === MallOrder::TYPE_PHYSICAL) { + if ($order->type !== MallOrder::TYPE_WITHDRAW) { return $this->error(__('Order type not supported')); } @@ -214,6 +214,9 @@ class Order extends Backend if ($order->status !== MallOrder::STATUS_PENDING) { return $this->error(__('Order status must be PENDING')); } + if (!in_array($order->type, [MallOrder::TYPE_PHYSICAL, MallOrder::TYPE_WITHDRAW], true)) { + return $this->error(__('Order type not supported')); + } if ($order->type === MallOrder::TYPE_PHYSICAL && $rejectReason === '') { return $this->error(__('Missing required fields')); @@ -291,7 +294,7 @@ class Order extends Backend return $this->error(__('Current grant status cannot be manually pushed')); } - if (strval(config('playx.api.base_url', '')) === '') { + if (strval(config('playx.angpow_import.base_url', '')) === '') { return $this->error(__('PlayX API not configured')); } diff --git a/app/common/library/MallBonusGrantPush.php b/app/common/library/MallBonusGrantPush.php index b32cfe4..f9d5244 100644 --- a/app/common/library/MallBonusGrantPush.php +++ b/app/common/library/MallBonusGrantPush.php @@ -6,6 +6,7 @@ namespace app\common\library; use app\common\model\MallItem; use app\common\model\MallOrder; +use app\common\model\MallUserAsset; use GuzzleHttp\Client; /** @@ -18,60 +19,117 @@ final class MallBonusGrantPush */ public static function push(MallOrder $order): array { - $baseUrl = rtrim(strval(config('playx.api.base_url', '')), '/'); - if ($baseUrl === '') { + $conf = config('playx.angpow_import'); + if (!is_array($conf)) { return [ 'ok' => false, - 'message' => 'PlayX base_url not configured', + 'message' => 'PlayX angpow_import not configured', + 'playx_transaction_id' => '', + ]; + } + + $baseUrl = rtrim(strval($conf['base_url'] ?? ''), '/'); + $path = strval($conf['path'] ?? ''); + $merchantCode = strval($conf['merchant_code'] ?? ''); + $authKey = strval($conf['auth_key'] ?? ''); + if ($baseUrl === '' || $path === '' || $merchantCode === '' || $authKey === '') { + return [ + 'ok' => false, + 'message' => 'PlayX Angpow Import API not configured', 'playx_transaction_id' => '', ]; } - $path = strval(config('playx.api.bonus_grant_url', '/api/v1/bonus/grant')); $url = $baseUrl . $path; + $asset = MallUserAsset::where('playx_user_id', $order->user_id)->find(); + if (!$asset || !is_string($asset->playx_user_id ?? null) || strval($asset->playx_user_id) === '') { + return [ + 'ok' => false, + 'message' => 'User asset not found', + 'playx_transaction_id' => '', + ]; + } + $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) : ''; + if (!$item) { + return [ + 'ok' => false, + 'message' => 'Item not found', + 'playx_transaction_id' => '', + ]; + } + + $reportDate = strval(time()); + $signatureInput = 'merchant_code=' . $merchantCode . '&report_date=' . $reportDate; + $signature = self::buildSignature($signatureInput, $authKey); + if ($signature === null) { + return [ + 'ok' => false, + 'message' => 'Build signature failed', + 'playx_transaction_id' => '', + ]; + } + + $start = gmdate('Y-m-d\TH:i:s\Z', strtotime(strval($order->start_time))); + $end = gmdate('Y-m-d\TH:i:s\Z', strtotime(strval($order->end_time))); $multiplier = intval($order->multiplier ?? 0); if ($multiplier <= 0) { $multiplier = 1; } + $payload = [ + 'merchant_code' => $merchantCode, + 'report_date' => $reportDate, + 'angpow' => [ + [ + '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' => $multiplier, + ], + ], + 'currency_visual' => [ + [ + 'currency' => strval($conf['currency'] ?? 'MYR'), + 'visual_name' => strval($conf['visual_name'] ?? 'Angpow'), + ], + ], + ]; + $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, + 'headers' => [ + 'Content-Type' => 'application/json', + 'X-Request-Signature' => $signature, ], + 'json' => $payload, ]); $data = json_decode(strval($res->getBody()), true) ?? []; - if ($res->getStatusCode() === 200 && ($data['status'] ?? '') === 'accepted') { + if (($data['code'] ?? null) === '0' || ($data['code'] ?? null) === 0) { return [ 'ok' => true, 'message' => '', - 'playx_transaction_id' => strval($data['playx_transaction_id'] ?? ''), + 'playx_transaction_id' => '', ]; } return [ 'ok' => false, - 'message' => strval($data['message'] ?? 'PlayX bonus grant not accepted'), + 'message' => strval($data['message'] ?? 'PlayX angpow import not accepted'), 'playx_transaction_id' => '', ]; } catch (\Throwable $e) { @@ -82,4 +140,35 @@ final class MallBonusGrantPush ]; } } + + private static 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); + } } diff --git a/app/process/AngpowImportJobs.php b/app/process/AngpowImportJobs.php index bcd0026..a631adc 100644 --- a/app/process/AngpowImportJobs.php +++ b/app/process/AngpowImportJobs.php @@ -247,8 +247,8 @@ class AngpowImportJobs // } // } - $start = gmdate('Y-m-d H:i:s', strtotime($order->start_time)); - $end = gmdate('Y-m-d H:i:s', strtotime($order->end_time)); + $start = gmdate('Y-m-d\TH:i:s\Z', strtotime($order->start_time)); + $end = gmdate('Y-m-d\TH:i:s\Z', strtotime($order->end_time)); return [ 'member_login' => strval($asset->playx_user_id), diff --git a/web/src/lang/autoload.ts b/web/src/lang/autoload.ts index af5d708..c0bbe4e 100644 --- a/web/src/lang/autoload.ts +++ b/web/src/lang/autoload.ts @@ -11,4 +11,8 @@ export default { [adminBaseRoutePath + '/user/rule']: ['./backend/${lang}/auth/rule.ts'], [adminBaseRoutePath + '/user/scoreLog']: ['./backend/${lang}/user/moneyLog.ts'], [adminBaseRoutePath + '/crud/crud']: ['./backend/${lang}/crud/log.ts', './backend/${lang}/crud/state.ts'], + [adminBaseRoutePath + '/mall/item']: ['./backend/${lang}/mall/item.ts'], + [adminBaseRoutePath + '/mall/item/index']: ['./backend/${lang}/mall/item.ts'], + [adminBaseRoutePath + '/mall.Item']: ['./backend/${lang}/mall/item.ts'], + [adminBaseRoutePath + '/mall.Item/index']: ['./backend/${lang}/mall/item.ts'], } diff --git a/web/src/lang/backend/en/mall/item.ts b/web/src/lang/backend/en/mall/item.ts index b6f41d0..0b67133 100644 --- a/web/src/lang/backend/en/mall/item.ts +++ b/web/src/lang/backend/en/mall/item.ts @@ -1,23 +1,26 @@ export default { - id: 'id', - title: 'title', - description: 'description', - remark: 'remark', - score: 'score', - type: 'type', - 'type 1': 'type 1', - 'type 2': 'type 2', - 'type 3': 'type 3', - amount: 'amount', - multiplier: 'multiplier', - category: 'category', - category_title: 'category_title', - admin_id: 'admin_id', - admin__username: 'username', - image: 'show image', - stock: 'stock', - sort: 'sort', - create_time: 'create_time', - update_time: 'update_time', - 'quick Search Fields': 'id', + id: 'ID', + title: 'Title', + description: 'Description', + remark: 'Remark', + score: 'Points', + type: 'Type', + 'type 1': 'Bonus', + 'type 2': 'Physical', + 'type 3': 'Withdraw', + amount: 'Cash amount', + multiplier: 'Turnover multiplier', + category: 'Category', + category_title: 'Category title', + admin_id: 'Created by', + admin__username: 'Created by', + image: 'Image', + stock: 'Stock', + sort: 'Sort', + status: 'Status', + 'status 0': 'Disabled', + 'status 1': 'Enabled', + create_time: 'Created at', + update_time: 'Updated at', + 'quick Search Fields': 'ID', } diff --git a/web/src/views/backend/mall/order/index.vue b/web/src/views/backend/mall/order/index.vue index 33d5a05..6efb7d2 100644 --- a/web/src/views/backend/mall/order/index.vue +++ b/web/src/views/backend/mall/order/index.vue @@ -38,6 +38,7 @@ const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete']).map((btn) type: 'primary', class: 'table-row-edit', icon: 'fa fa-check', + display: (row: TableRow) => ['PHYSICAL', 'WITHDRAW'].includes(String(row.type)) && row.status === 'PENDING', } : btn ) diff --git a/web/src/views/backend/mall/order/popupForm.vue b/web/src/views/backend/mall/order/popupForm.vue index 87b057e..893424f 100644 --- a/web/src/views/backend/mall/order/popupForm.vue +++ b/web/src/views/backend/mall/order/popupForm.vue @@ -85,7 +85,7 @@ :placeholder="t('Please input field', { field: t('mall.order.reject_reason') })" /> - 确认后将驳回该订单并退回积分(红利/提现订单无需填写驳回原因)。 + 确认后将驳回该订单并退回积分(提现订单无需填写驳回原因)。 @@ -182,9 +182,10 @@ const { t } = useI18n() const isEdit = computed(() => baTable.form.operate === 'Edit') const isPending = computed(() => isEdit.value && baTable.form.items?.status === 'PENDING') +const needsReviewType = computed(() => ['PHYSICAL', 'WITHDRAW'].includes(String(baTable.form.items?.type || ''))) const isPhysical = computed(() => baTable.form.items?.type === 'PHYSICAL') -const canApprove = computed(() => isPending.value) -const usePagedActions = computed(() => isPending.value) +const canApprove = computed(() => isPending.value && needsReviewType.value) +const usePagedActions = computed(() => isPending.value && needsReviewType.value) const page = ref<1 | 2>(1) const action = ref<'approveShip' | 'reject' | null>(null)