From 9f3946253a48e1af5d9b59c4813a2333cd8b3afc Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Tue, 10 Jan 2023 16:51:27 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=8EOC=E5=AF=BC=E5=85=A5=E4=BA=A7=E5=93=81?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Admin/Providers/AdminServiceProvider.php | 4 +- .../Console/Commands/MigrateFromOpenCart.php | 390 ++++++++++++++++++ config/database.php | 19 + 3 files changed, 412 insertions(+), 1 deletion(-) create mode 100644 beike/Console/Commands/MigrateFromOpenCart.php diff --git a/beike/Admin/Providers/AdminServiceProvider.php b/beike/Admin/Providers/AdminServiceProvider.php index cdeff3ca..9adfdc5c 100644 --- a/beike/Admin/Providers/AdminServiceProvider.php +++ b/beike/Admin/Providers/AdminServiceProvider.php @@ -24,6 +24,7 @@ use Beike\Admin\View\Components\NoData; use Beike\Admin\View\Components\Sidebar; use Beike\Console\Commands\GenerateDatabaseDict; use Beike\Console\Commands\MakeRootAdminUser; +use Beike\Console\Commands\MigrateFromOpenCart; use Beike\Console\Commands\Sitemap; use Beike\Models\AdminUser; use Illuminate\Support\Facades\Config; @@ -83,8 +84,9 @@ class AdminServiceProvider extends ServiceProvider if ($this->app->runningInConsole()) { $this->commands([ MakeRootAdminUser::class, - Sitemap::class, + MigrateFromOpenCart::class, GenerateDatabaseDict::class, + Sitemap::class, ]); } } diff --git a/beike/Console/Commands/MigrateFromOpenCart.php b/beike/Console/Commands/MigrateFromOpenCart.php new file mode 100644 index 00000000..a394e09d --- /dev/null +++ b/beike/Console/Commands/MigrateFromOpenCart.php @@ -0,0 +1,390 @@ + + * @created 2023-01-03 18:57:53 + * @modified 2023-01-03 18:57:53 + */ + +namespace Beike\Console\Commands; + +use Beike\Admin\Services\ProductService; +use Beike\Models\Brand; +use Beike\Models\Product; +use Beike\Models\ProductDescription; +use Beike\Models\ProductSku; +use Illuminate\Console\Command; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\DB; + +class MigrateFromOpenCart extends Command +{ + public const PER_PAGE = 1000; + + public const LANG_MAPPING = [ + 'zh_cn' => 11, + 'es' => 6, + ]; + + protected $signature = 'migrate:oc'; + + protected $description = '从 OpenCart 迁移数据'; + + protected ConnectionInterface $ocdb; + + private $ocProductVariants; + + private $ocVariantDescriptions; + + private $ocVariantValues; + + private $ocVariantValueDescriptions; + + private $ocProductImages; + + private int $page = 1; + + public function __construct() + { + parent::__construct(); + $this->ocdb = DB::connection('opencart'); + } + + /** + * 导入OC产品数据 + */ + public function handle() + { + // $this->importBrands(); + // $this->importProducts(); + } + + /** + * 导入品牌数据 + */ + private function importBrands() + { + Brand::query()->truncate(); + $this->ocdb->table('manufacturer') + ->orderBy('manufacturer_id') + ->chunk(self::PER_PAGE, function ($ocBrands) { + $bkBrands = []; + foreach ($ocBrands as $ocBrand) { + $bkBrands[] = [ + 'id' => $ocBrand->manufacturer_id, + 'name' => $ocBrand->name, + 'first' => mb_strtoupper(mb_substr($ocBrand->name, 0, 1)), + 'logo' => $ocBrand->image, + 'sort_order' => $ocBrand->sort_order, + 'status' => $ocBrand->status, + 'created_at' => now(), + 'updated_at' => now(), + ]; + } + Brand::query()->insert($bkBrands); + }); + } + + /** + * 导入产品 + */ + private function importProducts() + { + $this->ocProductVariants = $this->ocdb->table('product_variant')->get()->groupBy('product_id'); + $this->ocVariantDescriptions = $this->ocdb->table('variant_description')->get()->groupBy('variant_id'); + $this->ocVariantValues = $this->ocdb->table('variant_value')->get()->keyBy('variant_value_id'); + $this->ocVariantValueDescriptions = $this->ocdb->table('variant_value_description')->get()->groupBy('variant_value_id'); + $this->ocProductImages = $this->ocdb->table('product_image')->get()->groupBy('product_id'); + + $this->clearData(); + $this->ocdb->table('product') + ->where('parent_id', 0) + // ->where('product_id', 1894) + // ->where('sku', '10012378') + ->orderBy('product_id') + ->chunk(self::PER_PAGE, function ($ocProducts) { + $this->importProduct($ocProducts); + }); + } + + /** + * 导入单个产品 + * @param Collection $ocProducts + */ + private function importProduct(Collection $ocProducts) + { + $total = $ocProducts->count(); + $ocProductIds = $ocProducts->pluck('product_id'); + $childProducts = $this->ocdb->table('product')->whereIn('parent_id', $ocProductIds)->get()->groupBy('parent_id'); + foreach ($ocProducts as $index => $ocProduct) { + dump("Start handle Page: $this->page - {$index}/{$total}"); + $ocProductId = $ocProduct->product_id; + $productVariants = $this->ocProductVariants[$ocProductId] ?? []; + $childProducts = $childProducts[$ocProductId] ?? []; + $bkProduct = $this->generateBeikeProduct($ocProduct, $productVariants, $childProducts); + (new ProductService)->create($bkProduct); + } + $this->page++; + } + + /** + * 构造 beike 产品 + */ + private function generateBeikeProduct($ocProduct, $productVariants, $childProducts) + { + $variables = $this->generateVariables($ocProduct, $childProducts); + $bkProduct = [ + 'active' => $ocProduct->status, + 'brand_id' => $ocProduct->manufacturer_id, + 'position' => $ocProduct->sort_order, + 'tax_class_id' => $ocProduct->tax_class_id, + 'variables' => json_encode($variables), + ]; + $bkProduct['descriptions'] = $this->generateDescriptions($ocProduct); + $bkProduct['images'] = [$ocProduct->image]; + $bkProduct['skus'] = $this->generateSkus($ocProduct, $productVariants, $childProducts, $variables); + + return $bkProduct; + } + + /** + * @return array[] + */ + private function generateVariables($ocProduct, $childProducts): array + { + $productIds = [$ocProduct->product_id]; + foreach ($childProducts as $childProduct) { + $productIds[] = $childProduct->product_id; + } + + $locales = locales(); + $items = $values = []; + foreach ($productIds as $productId) { + $productVariants = $this->ocProductVariants[$productId] ?? []; + foreach ($productVariants as $productVariant) { + $productVariantId = $productVariant->variant_id; + $productVariantValueId = $productVariant->variant_value_id; + + $variants = $this->ocVariantDescriptions[$productVariantId]; + $variantValue = $this->ocVariantValues[$productVariantValueId]; + $variantValueDescriptions = $this->ocVariantValueDescriptions[$productVariantValueId]; + + $variants = $variants->keyBy('language_id'); + $names = []; + foreach ($locales as $locale) { + $variant = $variants[self::LANG_MAPPING[$locale['code']]]; + $names[$locale['code']] = $variant->name; + } + + $valueNames = []; + $variantValueDescriptions = $variantValueDescriptions->keyBy('language_id'); + foreach ($locales as $locale) { + $variantValueDescription = $variantValueDescriptions[self::LANG_MAPPING[$locale['code']]]; + $valueNames[$locale['code']] = $variantValueDescription->name; + } + + $values[$productVariantValueId] = [ + 'variant_value_id' => $productVariantValueId, + 'name' => $valueNames, + 'image' => $variantValue->image, + ]; + + $items[$productVariantId] = [ + 'variant_id' => $productVariantId, + 'name' => $names, + 'values' => $values, + 'isImage' => (bool) $variantValue->image, + ]; + } + } + + $items = array_values($items); + foreach ($items as $index => $item) { + $items[$index]['values'] = array_values($item['values']); + } + + return $items; + } + + /** + * 生成 beike 产品描述 + * @param $ocProduct + * @return array + */ + private function generateDescriptions($ocProduct): array + { + $descriptions = []; + $locales = locales(); + $ocDescriptions = $this->ocdb->table('product_description') + ->where('product_id', $ocProduct->product_id) + ->get() + ->keyBy('language_id') + ->toArray(); + + foreach ($locales as $locale) { + $ocDescription = $ocDescriptions[self::LANG_MAPPING[$locale['code']]]; + $descriptions[$locale['code']] = [ + 'name' => $ocDescription->name, + 'content' => html_entity_decode($ocDescription->description), + ]; + } + + return $descriptions; + } + + /** + * 生成 beike 产品 SKU + * + * @param $ocProduct + * @param $productVariants + * @param $childProducts + * @param array $variables + * @return array + */ + private function generateSkus($ocProduct, $productVariants, $childProducts, array $variables): array + { + // 简单商品 + if (count($productVariants) == 0) { + return $this->generateSimpleSku($ocProduct); + } + + $masterSku = $childSkus = []; + // 有主商品 + if (count($productVariants) > 0) { + $masterSku = $this->generateMasterSku($ocProduct, $productVariants, $variables); + } + // 有子商品 + if (count($childProducts) > 0) { + $childSkus = $this->generateChildSkus($childProducts, $variables); + } + if ($masterSku) { + array_push($childSkus, $masterSku); + } + + return $childSkus; + } + + /** + * 获取简单商品 SKU + * @param $ocProduct + * @return array[] + */ + private function generateSimpleSku($ocProduct): array + { + $images = $this->ocProductImages[$ocProduct->product_id] ?? []; + + return [ + [ + 'image' => $images, + 'model' => $ocProduct->model, + 'sku' => $ocProduct->sku, + 'price' => $ocProduct->price, + 'origin_price' => $ocProduct->price, + 'cost_price' => $ocProduct->cost_price, + 'quantity' => $ocProduct->quantity, + 'variants' => null, + 'position' => $ocProduct->sort_order, + 'is_default' => 1, + ], + ]; + } + + /** + * 获取主商品 SKU + * @param $ocProduct + * @param $productVariants + * @param $variables + * @return array + */ + private function generateMasterSku($ocProduct, $productVariants, $variables): array + { + $variants = $this->generateVariants($productVariants, $variables); + $images = $this->ocProductImages[$ocProduct->product_id] ?? []; + + return [ + 'image' => $images, + 'model' => $ocProduct->model, + 'sku' => $ocProduct->sku, + 'price' => $ocProduct->price, + 'origin_price' => $ocProduct->price, + 'cost_price' => $ocProduct->cost_price, + 'quantity' => $ocProduct->quantity, + 'variants' => $variants, + 'position' => $ocProduct->sort_order, + 'is_default' => 1, + ]; + } + + /** + * 获取子商品SKU + * @param $childProducts + * @param $variables + * @return array + */ + private function generateChildSkus($childProducts, $variables): array + { + $items = []; + foreach ($childProducts as $ocProduct) { + $ocProductId = $ocProduct->product_id; + $productVariants = $this->ocProductVariants[$ocProductId] ?? []; + + $images = $this->ocProductImages[$ocProductId] ?? []; + $variants = $this->generateVariants($productVariants, $variables); + $items[] = [ + 'image' => $images, + 'model' => $ocProduct->model, + 'sku' => $ocProduct->sku, + 'price' => $ocProduct->price, + 'origin_price' => $ocProduct->price, + 'cost_price' => $ocProduct->cost_price, + 'quantity' => $ocProduct->quantity, + 'variants' => $variants, + 'position' => $ocProduct->sort_order, + 'is_default' => 0, + ]; + } + + return $items; + } + + /** + * 生成商品 SKU 多规格数据 + * + * @param $productVariants + * @param $variables + * @return array + */ + private function generateVariants($productVariants, $variables): array + { + $items = []; + foreach ($productVariants as $productVariant) { + $variantId = $productVariant->variant_id; + $variantValueId = $productVariant->variant_value_id; + foreach ($variables as $index1 => $variable) { + if ($variantId == $variable['variant_id']) { + foreach ($variable['values'] as $index2 => $value) { + if ($variantValueId == $value['variant_value_id']) { + $items[$index1] = (string) $index2; + } + } + } + } + } + + return $items; + } + + /** + * 清空产品相关数据 + */ + private function clearData() + { + Product::query()->truncate(); + ProductSku::query()->truncate(); + ProductDescription::query()->truncate(); + } +} diff --git a/config/database.php b/config/database.php index 8f72108c..382645e9 100644 --- a/config/database.php +++ b/config/database.php @@ -63,6 +63,25 @@ return [ ]) : [], ], + 'opencart' => [ + 'driver' => 'mysql', + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('OC_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => env('OC_PREFIX', ''), + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => 'InnoDB', + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + 'pgsql' => [ 'driver' => 'pgsql', 'url' => env('DATABASE_URL'),