From 94596baf133c01b4335cbf410c315c8987b60c6a Mon Sep 17 00:00:00 2001 From: JiaSheng Date: Thu, 25 Jan 2024 16:52:06 +0800 Subject: [PATCH] 1688 integration --- .../General/Open1688/SignatureUtil.php | 31 ++ app/Classes/General/Open1688Helper.php | 79 +++++ .../GetProductDetailLogic.php | 84 ++++++ .../KeywordSearchProductLogic.php | 36 +++ .../CreatePurchaseOrderTransactionLogic.php | 22 +- .../Open1688/Open1688Controller.php | 30 ++ .../Resources/TransactionDetailResource.php | 13 +- config/open1688.php | 11 + .../AlibabaProductOverviewComponent.vue | 34 +++ .../AlibabaProductDetailFormComponent.vue | 275 ++++++++++++++++++ .../forms/PurchaseOrderItemFormComponent.vue | 5 +- .../sections/AlibabaSiteSectionComponent.vue | 145 +++++++++ .../BookingDetailsSectionComponent.vue | 7 +- routes/api.php | 2 + routes/open1688.php | 8 + 15 files changed, 776 insertions(+), 6 deletions(-) create mode 100644 app/Classes/General/Open1688/SignatureUtil.php create mode 100644 app/Classes/General/Open1688Helper.php create mode 100644 app/Classes/Modules/Open1688/ControllersLogic/GetProductDetailLogic.php create mode 100644 app/Classes/Modules/Open1688/ControllersLogic/KeywordSearchProductLogic.php create mode 100644 app/Http/Controllers/Open1688/Open1688Controller.php create mode 100644 config/open1688.php create mode 100644 resources/assets/vue/components/bookings/elements/AlibabaProductOverviewComponent.vue create mode 100644 resources/assets/vue/components/bookings/forms/AlibabaProductDetailFormComponent.vue create mode 100644 resources/assets/vue/components/bookings/sections/AlibabaSiteSectionComponent.vue create mode 100644 routes/open1688.php diff --git a/app/Classes/General/Open1688/SignatureUtil.php b/app/Classes/General/Open1688/SignatureUtil.php new file mode 100644 index 00000000..91c21005 --- /dev/null +++ b/app/Classes/General/Open1688/SignatureUtil.php @@ -0,0 +1,31 @@ + $v ) { + $paramToSign = $k . $v; + Array_push ( $paramsToSign, $paramToSign ); + } + sort ( $paramsToSign ); + $implodeParams = implode ( $paramsToSign ); + $pathAndParams = $path . $implodeParams; + $sign = hash_hmac ( "sha1", $pathAndParams, config('open1688.app_secret'), true ); + $signHexWithLowcase = bin2hex ( $sign ); + $signHexUppercase = strtoupper ( $signHexWithLowcase ); + return $signHexUppercase; + } +} + +?> \ No newline at end of file diff --git a/app/Classes/General/Open1688Helper.php b/app/Classes/General/Open1688Helper.php new file mode 100644 index 00000000..70bbf8eb --- /dev/null +++ b/app/Classes/General/Open1688Helper.php @@ -0,0 +1,79 @@ + "{\"keyword\":\"$keyword\",\"beginPage\":$page,\"pageSize\":18,\"country\":\"en\"}", + ]; + + return self::getFromOpen1688($requestData, config('open1688.product_search_keyword_api')); + } + + public static function productDetail($offerId) { + $requestData = [ + 'offerDetailParam' => "{\"offerId\":$offerId,\"country\":\"en\"}", + ]; + + return self::getFromOpen1688($requestData, config('open1688.product_detail_api')); + } + + private static function getFromOpen1688($requestData, $apiName) { + $requestData["access_token"] = config('open1688.access_token'); + + $signaturedStr = self::generateRequestSignature($apiName, $requestData); + $url = config('open1688.url') . $apiName . "/" . config('open1688.app_key') . "?_aop_signature=" . $signaturedStr; + + + foreach($requestData as $k => $v) { + $url = $url . "&$k=$v"; + } + + try { + $client = new \GuzzleHttp\Client(['verify' => false]); + $response = $client->request('GET', $url); + $body = $response->getBody(); + $contents = json_decode($body, true); + + return $contents['result']['result']; + } catch(\GuzzleHttp\Exception\RequestException $exception){ + throw new MalformedRequestException(json_decode($exception->getResponse()->getBody()->getContents(), true)['error_message']); + } + + } + + private static function generateRequestSignature ($apiName, $requestData) { + $pathToSign = self::generateAPIPath($apiName); + $signaturedStr = SignatureUtil::signature ($pathToSign, $requestData); + + return $signaturedStr; + } + + private static function generateAPIPath($apiName) { + $urlResult = ""; + $defs = array ( + $urlResult, + "param2", + "/", + "1", + "/", + "com.alibaba.fenxiao.crossborder", + "/", + $apiName, + "/", + config('open1688.app_key') + ); + + $urlResult = implode ( $defs ); + + return $urlResult; + } + +} diff --git a/app/Classes/Modules/Open1688/ControllersLogic/GetProductDetailLogic.php b/app/Classes/Modules/Open1688/ControllersLogic/GetProductDetailLogic.php new file mode 100644 index 00000000..1ec60540 --- /dev/null +++ b/app/Classes/Modules/Open1688/ControllersLogic/GetProductDetailLogic.php @@ -0,0 +1,84 @@ + 'Get product detail', + 'message' => 'You have successfully get 1688 product detail' + ]; + } + + /** + * @param Request $request + * @return JsonResponse + * @throws ErrorException + */ + public function logic(Request $request) : JsonResponse + { + $result = Open1688Helper::productDetail($request->route('offer_id')); + $productAttribute = []; + $variants = []; + $uniqueVariants = []; + $variantInfo = []; + $priceList = []; + $variantKeyCombinationOrder = []; + + foreach ($result['productAttribute'] as $attr) { + $productAttribute[$attr['attributeName']] = array_key_exists($attr['attributeName'], $productAttribute) ? $productAttribute[$attr['attributeName']] . ", " . $attr['value'] : $attr['value']; + } + + $result['productAttribute'] = $productAttribute; + + foreach ($result['productSkuInfos'] as $sku) { + $uniqueVariantKey = ''; + foreach ($sku['skuAttributes'] as $skuAttr) { + if (count($variantKeyCombinationOrder) < count($sku['skuAttributes'])) { + array_push($variantKeyCombinationOrder, $skuAttr['attributeName']); + } + if (!in_array($skuAttr['value'], $uniqueVariants)) { + $variants[$skuAttr['attributeName']][] = [ + 'image' => isset($skuAttr['skuImageUrl']) ? $skuAttr['skuImageUrl'] : null, + 'value' => $skuAttr['value'] + ]; + } + + array_push($uniqueVariants, $skuAttr['value']); + + $uniqueVariantKey .= $skuAttr['value']; + } + + $price = isset($sku['price']) ? $sku['price'] : $sku['consignPrice']; + + $variantInfo[$uniqueVariantKey] = [ + 'skuId' => $sku['skuId'], + 'amountOnSale' => $sku['amountOnSale'], + 'price' => $price, + ]; + array_push($priceList, $price); + } + + $result['variants'] = $variants; + $result['variantInfo'] = $variantInfo; + + $minPrice = min($priceList); + $maxPrice = max($priceList); + + $result['priceRange'] = $minPrice === $maxPrice ? $minPrice : "CNY ¥$minPrice - CNY ¥$maxPrice"; + $result['variantKeyCombinationOrder'] = $variantKeyCombinationOrder; + + return $this->response(['data' => $result]); + } +} diff --git a/app/Classes/Modules/Open1688/ControllersLogic/KeywordSearchProductLogic.php b/app/Classes/Modules/Open1688/ControllersLogic/KeywordSearchProductLogic.php new file mode 100644 index 00000000..526c3b1f --- /dev/null +++ b/app/Classes/Modules/Open1688/ControllersLogic/KeywordSearchProductLogic.php @@ -0,0 +1,36 @@ + 'Search 1688 Products', + 'message' => 'You have successfully retrieved a list of products from 1688' + ]; + } + + /** + * @param Request $request + * @return JsonResponse + * @throws ErrorException + */ + public function logic(Request $request) : JsonResponse + { + $result = Open1688Helper::productSearchKeyword($request->input('keyword'), $request->input('page')); + + return $this->response($result); + } +} diff --git a/app/Classes/Modules/Transactions/ControllersLogic/CreatePurchaseOrderTransactionLogic.php b/app/Classes/Modules/Transactions/ControllersLogic/CreatePurchaseOrderTransactionLogic.php index 9bab107f..68072a6e 100644 --- a/app/Classes/Modules/Transactions/ControllersLogic/CreatePurchaseOrderTransactionLogic.php +++ b/app/Classes/Modules/Transactions/ControllersLogic/CreatePurchaseOrderTransactionLogic.php @@ -72,14 +72,32 @@ class CreatePurchaseOrderTransactionLogic extends AbstractControllerLogic $billNumber = $this->generatesTransactionBillNumber->execute('PO-'); - $total = collect($request->input('products'))->sum(function($product){ + $products = $request->input('products'); + + if ($request->input('pushToExisting')) { + $transaction = $booking->transactions()->where('type', TransactionType::PURCHASE_ORDER)->first(); + + if ($transaction) { + foreach ($transaction->transactionDetails as $detail) { + array_push($products, [ + 'stockCode' => $detail->product_code, + 'description' => $detail->product_name, + 'quantity' => $detail->quantity, + 'unit_price' => $detail->price, + 'total' => $detail->amount + ]); + } + } + } + + $total = collect($products)->sum(function($product){ return $product['quantity'] * floatval(str_replace(',', '', $product['unit_price'])); }); $object = new TransactionObject($billNumber, TransactionType::PURCHASE_ORDER, $booking->company->id, 1, 1, PaymentMethodType::CASH, $total, $total, $booking->fix_currency_id, $booking->fix_currency_id, - 1, 0, 0, null, ApprovalStatus::PENDING_SUBMISSION, $request->input('products')); + 1, 0, 0, null, ApprovalStatus::PENDING_SUBMISSION, $products); $transaction = $this->createPurchaseOrderTransactionProcessor->execute($booking, $object); diff --git a/app/Http/Controllers/Open1688/Open1688Controller.php b/app/Http/Controllers/Open1688/Open1688Controller.php new file mode 100644 index 00000000..2ed20540 --- /dev/null +++ b/app/Http/Controllers/Open1688/Open1688Controller.php @@ -0,0 +1,30 @@ +execute($request); + } + + /** + * @param Request $request + * @param GetProductDetailLogic $logic + * @return JsonResponse + */ + public function getProductDetail(Request $request, GetProductDetailLogic $logic): JsonResponse { + return $logic->execute($request); + } + +} diff --git a/app/Http/Resources/TransactionDetailResource.php b/app/Http/Resources/TransactionDetailResource.php index 4257a9fb..63c6661b 100644 --- a/app/Http/Resources/TransactionDetailResource.php +++ b/app/Http/Resources/TransactionDetailResource.php @@ -14,14 +14,23 @@ class TransactionDetailResource extends JsonResource */ public function toArray($request) { - + $item_from_1688 = str_contains($this->product_code, 'offerId'); + $offer_id_1688 = null; + if ($item_from_1688) { + $offerId = explode(',', $this->product_code)[0]; + $offer_id_1688 = substr($offerId, strpos($offerId, ':') + 1); + } + + return [ 'id' => $this->id, 'stockCode' => $this->product_code, 'description' => $this->product_name, 'quantity' => $this->quantity, 'unit_price' => (double) $this->price, - 'total' => (double) $this->amount + 'total' => (double) $this->amount, + 'item_from_1688' => $item_from_1688, + 'offer_id_1688' => $offer_id_1688 ]; } } diff --git a/config/open1688.php b/config/open1688.php new file mode 100644 index 00000000..7cf948f4 --- /dev/null +++ b/config/open1688.php @@ -0,0 +1,11 @@ + env('OPEN_1688_APP_KEY', ''), + 'app_secret' => env('OPEN_1688_APP_SECRET', ''), + 'access_token' => env('open_1688_ACCESS_TOKEN', ''), + 'refresh_token' => env('open_1688_REFRESH_TOKEN', ''), + 'url' => env('OPEN_1688_URL', ''), + 'product_search_keyword_api' => env('OPEN_1688_PRODUCT_SEARCH_KEYWORD', ''), + 'product_detail_api' => env('OPEN_1688_PRODUCT_DETAIL', ''), +]; diff --git a/resources/assets/vue/components/bookings/elements/AlibabaProductOverviewComponent.vue b/resources/assets/vue/components/bookings/elements/AlibabaProductOverviewComponent.vue new file mode 100644 index 00000000..7c5f43fa --- /dev/null +++ b/resources/assets/vue/components/bookings/elements/AlibabaProductOverviewComponent.vue @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/resources/assets/vue/components/bookings/forms/AlibabaProductDetailFormComponent.vue b/resources/assets/vue/components/bookings/forms/AlibabaProductDetailFormComponent.vue new file mode 100644 index 00000000..80ab17b0 --- /dev/null +++ b/resources/assets/vue/components/bookings/forms/AlibabaProductDetailFormComponent.vue @@ -0,0 +1,275 @@ + + \ No newline at end of file diff --git a/resources/assets/vue/components/bookings/forms/PurchaseOrderItemFormComponent.vue b/resources/assets/vue/components/bookings/forms/PurchaseOrderItemFormComponent.vue index d91d2661..16a10b64 100644 --- a/resources/assets/vue/components/bookings/forms/PurchaseOrderItemFormComponent.vue +++ b/resources/assets/vue/components/bookings/forms/PurchaseOrderItemFormComponent.vue @@ -22,7 +22,10 @@
-

{{product.description}}

+ +

{{product.description}}

+
+

{{product.description}}

diff --git a/resources/assets/vue/components/bookings/sections/AlibabaSiteSectionComponent.vue b/resources/assets/vue/components/bookings/sections/AlibabaSiteSectionComponent.vue new file mode 100644 index 00000000..a9afe1d8 --- /dev/null +++ b/resources/assets/vue/components/bookings/sections/AlibabaSiteSectionComponent.vue @@ -0,0 +1,145 @@ + + \ No newline at end of file diff --git a/resources/assets/vue/components/bookings/sections/BookingDetailsSectionComponent.vue b/resources/assets/vue/components/bookings/sections/BookingDetailsSectionComponent.vue index a2762ce7..1929aead 100644 --- a/resources/assets/vue/components/bookings/sections/BookingDetailsSectionComponent.vue +++ b/resources/assets/vue/components/bookings/sections/BookingDetailsSectionComponent.vue @@ -110,10 +110,15 @@
+
+
+ +
+
-
+
diff --git a/routes/api.php b/routes/api.php index 8b4bada0..715bd9d2 100644 --- a/routes/api.php +++ b/routes/api.php @@ -67,6 +67,8 @@ Route::group(['middleware' => 'api', 'prefix' => 'v1', 'as' => 'api.'], function require __DIR__ . '/milestone.php'; + require __DIR__ . '/open1688.php'; + // require __DIR__ . '/rate.php'; // require __DIR__ . '/receipt.php'; diff --git a/routes/open1688.php b/routes/open1688.php new file mode 100644 index 00000000..93250f21 --- /dev/null +++ b/routes/open1688.php @@ -0,0 +1,8 @@ + 'open1688', 'namespace' => 'Open1688', 'as' => 'open1688.'], function () { + Route::get('/', 'Open1688Controller@keywordSearchProduct')->name('keywordSearchProduct'); + Route::get('/{offer_id}', 'Open1688Controller@getProductDetail')->name('getProductDetail'); +});