From 5dbb434018b490da3369c73306e9544806074b89 Mon Sep 17 00:00:00 2001 From: jsasg <735273025@qq.com> Date: Thu, 8 May 2025 18:19:46 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E6=95=B0=E6=8D=AE=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E7=A8=8B=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/command/DataMigration.php | 488 ++++++++++++++++++++++++++++++---- 1 file changed, 431 insertions(+), 57 deletions(-) diff --git a/app/command/DataMigration.php b/app/command/DataMigration.php index 61d9918f..1b48d496 100644 --- a/app/command/DataMigration.php +++ b/app/command/DataMigration.php @@ -26,9 +26,12 @@ class DataMigration extends Command protected function execute(Input $input, Output $output) { + ini_set('pcre.backtrack_limit', 10000000); + ini_set('default_socket_timeout', 1); + // 指令输出 $this->println = function ($msg) use ($output) { - $output->writeln($msg); + $output->writeln($msg); }; try { @@ -38,15 +41,30 @@ class DataMigration extends Command // 迁移产品分类 // $this->productCategory(); + // 迁移产品属性 + // $this->migrateProductAttr(); + + // 迁移产品 + $this->migrateProduct(); + // 迁移文章 // $this->migrateArticle([ - // 1 => 2, - // 2 => 3, - // 33 => 4, - // 36 => 5, - // 16 => 7, - // 31 => 8, - // 32 => 9 + // 68 => 10, + // 69 => 11, + // 70 => 12, + // 71 => 13, + // 72 => 14, + // 73 => 15, + // 74 => 16, + // 75 => 17, + // 78 => 19, + // 79 => 20, + // 80 => 21, + // 81 => 22, + // 82 => 23, + // 83 => 24, + // 84 => 25, + // 85 => 26 // ]); // 迁移faq @@ -72,29 +90,31 @@ class DataMigration extends Command // $this->migrateVideoCategory(); // 迁移视频 - $this->migrateVideo([ - 0 => 0, - 1 => 1, - 2 => 2, - 3 => 3, - 4 => 4, - 5 => 5, - 6 => 6, - 7 => 7, - 8 => 8, - 11 => 0, - 36 => 9, - 37 => 10, - 38 => 11, - 39 => 12, - 40 => 13, - 41 => 14, - 42 => 15, - 43 => 16, - 60 => 17, - 62 => 18, - 63 => 19 - ]); + // $this->migrateVideo([ + // 0 => 0, + // 1 => 1, + // 2 => 2, + // 3 => 3, + // 4 => 4, + // 5 => 5, + // 6 => 6, + // 7 => 7, + // 8 => 8, + // 11 => 0, + // 36 => 9, + // 37 => 10, + // 38 => 11, + // 39 => 12, + // 40 => 13, + // 41 => 14, + // 42 => 15, + // 43 => 16, + // 60 => 17, + // 62 => 18, + // 63 => 19 + // ]); + + // $this->test(); $output->writeln('success'); } catch(\Throwable $th) { @@ -174,6 +194,319 @@ class DataMigration extends Command } } + // 迁移产品属性 + private function migrateProductAttr() + { + $products = Db::connect('old') + ->name('product') + ->where('country_code', 'in', ['ZH', 'US']) + ->where('product_attr', '<>', '') + ->order(['id' => 'asc']) + ->cursor(); + + $exists = []; + foreach ($products as $v) + { + $attrs = []; + $product_attr = json_decode($v['product_attr'], true); + foreach ($product_attr as $attr) { + if (!in_array($attr, ['颜色', 'Color']) && !in_array($attr, $exists)) { + $attrs[] = [ + 'language_id' => $v['country_code'] == 'ZH' ? 1 : 2, + 'attr_type' => 2, + 'attr_name' => $attr, + 'is_system' => 0 + ]; + $exists[] = $attr; + } + } + if (empty($attrs)) { + continue; + } + Db::name('product_attr')->insertAll($attrs); + } + } + + // 迁移产品 + private function migrateProduct() + { + $attrs_map = []; + $attrs_dict = Db::name('product_attr') + ->withoutField(['created_at', 'updated_at', 'deleted_at']) + ->select(); + foreach ($attrs_dict as $attr) + { + $code = $attr['language_id'] == 1 ? 'ZH' : 'US'; + if (!isset($attrs_map[$code])) { + $attrs_map[$code] = []; + } + $attrs_map[$code][$attr['attr_name']] = $attr['id']; + } + + $old_db = Db::connect('old'); + + $products = $old_db->name('product') + ->where('id', '=', 128) + ->where('country_code', 'in', ['ZH', 'US']) + ->order(['id' => 'asc']) + ->cursor(); + + $uploadMgr = new UploadMannager(); + foreach ($products as $v) + { + Db::startTrans(); + try { + // 处理封面图片 + $image = ''; + $image_ret = $uploadMgr->upload($uploadMgr->download($v['list_bk_img']), 'image', 'product'); + if ($image_ret['code'] == 0) { + $image = $image_ret['data']['path']; + } else { + $image = $image_ret['msg']; + } + + // 处理视频 + $video = ''; + $video_ret = $uploadMgr->upload($uploadMgr->download($v['videopath']), 'video', 'video'); + if ($video_ret['code'] == 0) { + $video = $video_ret['data']['path']; + } else { + $video = $video_ret['msg']; + } + + // 处理详情中图片 + $content = preg_replace_callback('/]*src=[\'"]([^\'"]*?)[\'"][^>]*>/is', function($matches) use ($uploadMgr) { + $file_path = ''; + try { + $ret = $uploadMgr->upload($uploadMgr->download($matches[1]), 'image'); + if ($ret['code'] == 0) { + $file_path = $ret['data']['path']; + } + } catch (\Throwable $th) { + $this->println($th->getMessage() . ':' . $th->getLine()); + return ''; + } + return ''; + }, $v['ld_md_content']); + + $item = [ + 'language_id' => $v['country_code'] == 'ZH' ? 1 : 2, + 'category_id' => '', + 'spu' => $v['brand_id'], + 'name' => $v['name'], + 'short_name' => $v['shortname'], + 'cover_image' => $image, + 'desc' => $v['description'], + 'video_img' => '', + 'video_url' => $video, + 'is_sale' => $v['is_onsale'], + 'is_new' => $v['isnew'], + 'is_hot' => $v['ishot'], + 'is_show' => $v['is_show'], + 'sort' => $v['sort'], + 'detail' => $content, + 'status' => $v['stat'] == -1 ? -1 : 1, + 'seo_title' => $v['seo_title'], + 'seo_keywords' => $v['seo_keyword'], + 'seo_desc' => $v['seo_description'], + 'created_at' => date('Y-m-d H:i:s', $v['createtime']), + 'updated_at' => date('Y-m-d H:i:s', $v['updatetime']), + 'deleted_at' => $v['stat'] == -1 ? date('Y-m-d H:i:s') : null, + ]; + // 保存产品数据 + $id = Db::name('product')->insertGetId($item); + + // 保存产品参数数据 + $prarms = []; + $views = unserialize($v['product_view']); + foreach ($views as $p) { + $prarms[] = [ + 'product_id' => $id, + 'name' => $p['desc_title'], + 'value' => $p['desc_desc'] + ]; + } + Db::name('product_params')->insertAll($prarms); + + // 保存sku数据 + $skus = []; + $sku_images = $old_db->name('product_image') + ->where('product_id', '=', $v['id']) + ->where('country_code', '=', $v['country_code']) + ->where('stat', '=', 0) + ->order(['id' => 'asc']) + ->select(); + $images = []; + $photo_album = []; + foreach ($sku_images as $im) { + $pkey = $im['sku']; + if (empty($pkey)) { + if (!empty($im['image_color'])) { + $pkey = md5($im['image_color']); + } else { + $pkey = 0; + } + } + if (!isset($images[$pkey])) { + $images[$pkey] = []; + $images[$pkey]['sku'] = $im['sku']; + } + + // 处理图册 + if (!empty($im['image_url'])) { + $photos = json_decode($im['image_url'], true); + if (empty($photos)) { + $photos[] = $im['image_url']; + } + foreach ($photos as $photo) { + $photos_ret = $uploadMgr->upload($uploadMgr->download($photo), 'image', 'product'); + if ($photos_ret['code'] == 0) { + if (!isset($photo_album[$pkey])) { + $photo_album[$pkey] = []; + } + $photo_album[$pkey][] = $photos_ret['data']['path']; + } + } + } + + // 处理属性 + $attrs = json_decode($im['image_color'], true); + if (!empty($attrs)) { + foreach ($attrs as $k => $at) { + if ($k != 'sort') { + $attr_value = $at; + if (in_array($k, ['颜色', 'Color'])) { + if ($k == 'Color') $k = '颜色'; + $attr_value = '/static/common/images/colors/' . $at . '.png'; + } + $images[$pkey]['attrs'] = [ + 'attr_id' => $attrs_map[$v['country_code']][$k], + 'attr_value' => $attr_value, + ]; + } + } + } else { + $attr_value = $im['image_color']; + if (empty($attr_value) || $attr_value == '[]') { + $images[$pkey]['attrs'] = []; + continue; + } + if (\think\helper\Str::endsWith($attr_value, ['.png', '.jpg', 'jpeg', '.gif'])) { + $photos_ret = $uploadMgr->upload($uploadMgr->download($attr_value), 'image', 'product'); + if ($photos_ret['code'] == 0) { + $attr_value = $photos_ret['data']['path']; + } + } + $attr_arr = [ + 'attr_id' => $attrs_map[$v['country_code']]['颜色'], + 'attr_value' => $attr_value, + ]; + if ( + empty($images[$pkey]['attrs']) || + (!empty($images[$pkey]['attrs']) && empty(array_intersect($images[$pkey]['attrs'], $attr_arr))) + ) { + + $images[$pkey]['attrs'][] = $attr_arr; + file_put_contents(runtime_path() . 'attrs.txt', json_encode($attr_arr)); + } + } + } + foreach ($images as $key => $image) { + $images[$key]['photo_album'] = $photo_album[$key]; + } + file_put_contents(runtime_path() . 'images.txt', json_encode($images, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + file_put_contents(runtime_path() . 'photo_album.txt', json_encode($photo_album, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + + $two_images = $old_db->name('product_two_img') + ->where('product_id', '=', $v['id']) + ->where('country_code', '=', $v['country_code']) + ->where('stat', '=', 0) + ->order(['id' => 'asc']) + ->select(); + foreach ($two_images as $ti) { + $tpkey = 0; + if (!empty($ti['image_color'])) { + $tpkey = md5($ti['image_color']); + } + if (!empty($ti['image_url'])) { + $main_image_ret = $uploadMgr->upload($uploadMgr->download($ti['image_url']), 'image', 'product'); + if ($main_image_ret['code'] == 0) { + $ti['image_url'] = $main_image_ret['data']['path']; + } + } + $skus[] = ['main_image' => $ti['image_url'], 'image_color' => $ti['image_color'], 'pkey' => $tpkey]; + } + if (!empty($skus)) { + $temp = []; + foreach ($skus as $idx => $sku) { + if (isset($images[$sku['pkey']])) { + $skus[$idx]['product_id'] = $id; + $skus[$idx]['sku'] = $images[$sku['pkey']]['sku']; + $skus[$idx]['photo_album'] = $images[$sku['pkey']]['photo_album']; + $skus[$idx]['attrs'] = $images[$sku['pkey']]['attrs']; + } else { + if (!empty($images[$idx])) { + $temp = $images[$idx]; + } + if (!empty($temp)) { + $skus[$idx]['product_id'] = $id; + $skus[$idx]['sku'] = $temp['sku']; + $skus[$idx]['photo_album'] = $temp['photo_album']; + $skus[$idx]['attrs'] = $temp['attrs']; + } + } + unset($skus[$idx]['pkey']); + unset($skus[$idx]['image_color']); + } + } else { + foreach ($images as $image) { + $skus[] = [ + 'product_id' => $id, + 'sku' => $image['sku'], + 'main_image' => '', + 'photo_album' => $image['photo_album'], + 'skus' => $image['attrs'] + ]; + } + } + file_put_contents(runtime_path() . 'skus.txt', json_encode($skus, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + // throw new \Exception("exit;"); + + foreach ($skus as $sku) { + $sku_id = Db::name('product_sku')->insertGetId([ + 'product_id' => $sku['product_id'], + 'sku' => $sku['sku'], + 'main_image' => $sku['main_image'], + 'photo_album' => json_encode($sku['photo_album']), + ]); + if (!empty($sku['attrs'])) { + foreach ($sku['attrs'] as $attr) { + if (!empty($sku_id)) { + Db::name('product_sku_attr')->insert([ + 'sku_id' => $sku_id, + 'attr_id' => $attr['attr_id'], + 'attr_value' => $attr['attr_value'] + ]); + } + } + } + } + Db::commit(); + $this->println(sprintf('迁移产品ID:%s => %s', $v['id'], $id)); + } catch (\Throwable $th) { + Db::rollback(); + file_put_contents( + runtime_path() . 'product_throwable.txt', + sprintf('【%s】 产品【%s】,迁移失败,错误【%s:%d】' . PHP_EOL, date('Y-h-d H:i:s'), $v['id'], $th->getMessage(), $th->getLine()) + ); + file_put_contents( + runtime_path() . 'product_throwable_details.txt', + (string)$th + ); + } + } + } + // 迁移文章 private function migrateArticle($category_map = []) { @@ -201,22 +534,19 @@ class DataMigration extends Command } // 处理详情中图片 - $content = $v['content']; - preg_match_all('//i', $content, $matches); - $content_images = []; - foreach ($matches[1] as $val) { + $content = preg_replace_callback('/]*src=[\'"]([^\'"]*?)[\'"][^>]*>/is', function ($matches) use ($uploadMgr) { + $file_path = ''; try { - $ret = $uploadMgr->upload($uploadMgr->download($val), 'image'); + $ret = $uploadMgr->upload($uploadMgr->download($matches[1]), 'image'); if ($ret['code'] == 0) { - $content_images[$val] = $ret['data']['path']; + $file_path = $ret['data']['path']; } } catch (\Throwable $th) { - continue; + $this->println($th->getMessage() . ':' . $th->getLine()); + return ''; } - } - foreach ($content_images as $key => $val) { - $content = str_replace($key, $val, $content); - } + return ''; + }, $v['content']); $item = [ 'language_id' => $v['country_code'] == 'ZH' ? 1 : 2, 'category_id' => $category_map[$v['cid']], @@ -234,12 +564,12 @@ class DataMigration extends Command 'seo_title' => $v['seo_title'], 'seo_keywords' => $v['seo_keyword'], 'seo_desc' => $v['seo_description'], - 'enabled' => $v['stat'], - 'release_time' => date('Y-m-d H:i:s', $v['createtime']) + 'release_time' => date('Y-m-d H:i:s', $v['createtime']), + 'deleted_at' => $v['stat'] == -1 ? date('Y-m-d H:i:s') : null ]; - Db::name('article')->insert($item); + $id = Db::name('article')->insertGetId($item); - $this->println('迁移文章ID:' . $v['id']); + $this->println(sprintf('迁移文章ID:%s => %s', $v['id'], $id)); } } @@ -422,7 +752,7 @@ class DataMigration extends Command class UploadMannager { const UPLOAD_BASE_API = 'http://dev.ow.f2b211.com'; - const DOWNLOAD_BASE_API = 'https://www.orico.com.cn'; + const DOWNLOAD_BASE_API = 'http://www.orico.com.cn'; const DOWNLOAD_TEMP_PATH = '/var/www/html/orico-official-website/public/migrate_temp_images'; private $username = 'admin'; private $password = 'Aa-1221'; @@ -453,20 +783,44 @@ class UploadMannager $url = self::DOWNLOAD_BASE_API . \think\helper\Str::substr($file_name, mb_strpos($file_name, $need) + mb_strlen($need)); } $file_name = '/' . \think\helper\Str::substr($url, mb_strpos($url, '://') + 3); - } else { + } + elseif (\think\helper\Str::startsWith($file_name, 'data:image/')) { + $idx = strpos($file_name, ';'); + $file_type = substr($file_name, 0, $idx); + $base64_image = preg_replace('#^data:image/\w+;base64,#i', '', $file_name); + $file_data = base64_decode($base64_image, true); + $file_path = self::DOWNLOAD_TEMP_PATH . '/uploads/' . date('Ymd') . '/' . uniqid() . '.' . substr($file_type, 11); + $dir = dirname($file_path); + if (!is_dir($dir)) { + mkdir($dir, 0777, true); + } + if (!file_put_contents($file_path, $file_data, FILE_USE_INCLUDE_PATH)) { + throw new \Exception('转换base64图片失败'); + } + return $file_path; + } + else { $url = self::DOWNLOAD_BASE_API . str_replace(" ", "%20", $file_name); } $file_path = self::DOWNLOAD_TEMP_PATH . $file_name; - $opts = [ - 'http' => [ - 'method' => 'GET', - 'header' => 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)' - ] - ]; - $context = stream_context_create($opts); - $file = file_get_contents($url, false, $context); - $dir = dirname($file_path); + if (file_exists($file_path)) { + return $file_path; + } + + // 使用file_get_contents下载 + // $opts = [ + // 'http' => [ + // 'method' => 'GET', + // 'header' => 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)' + // ] + // ]; + // $context = stream_context_create($opts); + // $file = file_get_contents($url, false, $context); + + // 使用curl下载 + $file = $this->file_get_withcurl($url); + $dir = dirname($file_path); if (!is_dir($dir)) { mkdir($dir, 0777, true); } @@ -474,6 +828,26 @@ class UploadMannager return $file_path; } + // 使用curl下载图片 + private function file_get_withcurl($url) + { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // 连接超时 10 秒 + curl_setopt($ch, CURLOPT_TIMEOUT, 30); // 传输超时 30 秒 + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // 跟随重定向 + curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0'); // 模拟浏览器 UA + + $data = curl_exec($ch); + if ($data === false) { + error_log(sprintf('cURL Error: %s; URL: %s', curl_error($ch), $url)); + return false; + } + curl_close($ch); + + return $data; + } + // 上传图片 public function upload($file_path, $field_name, $module = 'unknown') { if (empty($file_path)) { From 1d71ee3548151886e1cf41472a6a166faa4c0b42 Mon Sep 17 00:00:00 2001 From: jsasg <735273025@qq.com> Date: Fri, 9 May 2025 10:47:25 +0800 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20=E4=BF=AE=E6=94=B9=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E6=97=B6=E4=BA=A7=E5=93=81=E5=8F=82=E6=95=B0=E6=AD=A3?= =?UTF-8?q?=E5=88=99=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/controller/v1/Product.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/admin/controller/v1/Product.php b/app/admin/controller/v1/Product.php index d94aeea5..98e1080e 100644 --- a/app/admin/controller/v1/Product.php +++ b/app/admin/controller/v1/Product.php @@ -170,7 +170,7 @@ class Product // 更新产品参数 if ($put['params'] != "") { ProductParamsModel::productId($id)->delete(); - if (preg_match_all('/(\w+):(.[^\n|\r|\r\n]+)/', $put['params'], $match_result)) { + if (preg_match_all('/(\S+):(.[^\s]+)/', $put['params'], $match_result)) { $params = []; for ($i = 0; $i < count($match_result[0]); $i++) { $params[] = [ From c7f95bba3a4969db4c428911a8496d4000069b25 Mon Sep 17 00:00:00 2001 From: jsasg <735273025@qq.com> Date: Fri, 9 May 2025 10:53:22 +0800 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20=E4=BA=A7=E5=93=81=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E9=A1=B5=E5=8F=96=E5=89=AF=E6=A0=87=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/index/view/product/detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/index/view/product/detail.html b/app/index/view/product/detail.html index 9bc9a864..6c43d3ee 100644 --- a/app/index/view/product/detail.html +++ b/app/index/view/product/detail.html @@ -75,7 +75,7 @@

{$product.name|default=''}

-

{$product.desc|default=''}

+

{$product.short_name|default=''}

    {volist name="product_params" id="pp"} From e648538fe5a365a7271da5ed06098473e4db5a30 Mon Sep 17 00:00:00 2001 From: jsasg <735273025@qq.com> Date: Fri, 9 May 2025 11:08:52 +0800 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20=E4=BF=AE=E6=94=B9=E4=BA=A7?= =?UTF-8?q?=E5=93=81sku=E5=B1=9E=E6=80=A7=E5=80=BC=E9=95=BF=E5=BA=A6?= =?UTF-8?q?=E9=99=90=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/validate/v1/ProductValidate.php | 2 +- database/migrations/20241219074458_create_product_sku_attr.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/admin/validate/v1/ProductValidate.php b/app/admin/validate/v1/ProductValidate.php index f19900bc..1a1f2d30 100644 --- a/app/admin/validate/v1/ProductValidate.php +++ b/app/admin/validate/v1/ProductValidate.php @@ -39,7 +39,7 @@ class ProductValidate extends Validate 'skus.*.sku' => 'max:125', 'skus.*.main_image' => 'max:255', 'skus.*.sort' => 'integer', - 'skus.*.attrs' => 'checkSkusAttrsItemType:attr_id,integer|checkSkusAttrsItemMax:attr_value,64', + 'skus.*.attrs' => 'checkSkusAttrsItemType:attr_id,integer|checkSkusAttrsItemMax:attr_value,128', 'related.*.related_product_id' => 'integer', 'related.*.sort' => 'integer', ]; diff --git a/database/migrations/20241219074458_create_product_sku_attr.php b/database/migrations/20241219074458_create_product_sku_attr.php index f451e5d7..1f12533a 100644 --- a/database/migrations/20241219074458_create_product_sku_attr.php +++ b/database/migrations/20241219074458_create_product_sku_attr.php @@ -30,7 +30,7 @@ class CreateProductSkuAttr extends Migrator $table = $this->table('product_sku_attr', ['id' => false,'engine' => 'InnoDB', 'comment' => '产品SKU属性表']); $table->addColumn('sku_id', 'integer', ['signed' => false, 'null' => false, 'comment' => '产品SKU ID']) ->addColumn('attr_id', 'integer', ['signed' => false, 'null' => false, 'comment' => '属性ID']) - ->addColumn('attr_value', 'string', ['limit' => 64, 'null' => false, 'default' => '', 'comment' => '属性值']) + ->addColumn('attr_value', 'string', ['limit' => 128, 'null' => false, 'default' => '', 'comment' => '属性值']) ->addForeignKey('sku_id', 'product_sku', 'id', ['update' => 'CASCADE', 'delete' => 'CASCADE']) ->create(); }