From 9883500a0e457483e3b5972758bf3cb7c8d403b2 Mon Sep 17 00:00:00 2001 From: "WIN-C27C5MSVTPO\\16961" <1696136552@qq.com> Date: Tue, 16 Dec 2025 10:16:25 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8E=8B=E7=BC=A9=E5=8C=85=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/controller/SkuController.php | 121 ++++++++++++++- composer.json | 3 +- composer.lock | 206 +++++++++++++++++++++++-- 3 files changed, 304 insertions(+), 26 deletions(-) diff --git a/app/admin/controller/SkuController.php b/app/admin/controller/SkuController.php index 658e0af..dff7e8f 100644 --- a/app/admin/controller/SkuController.php +++ b/app/admin/controller/SkuController.php @@ -10,10 +10,12 @@ use app\common\service\UploadService; use app\common\traits\crud\CrudControllerTrait; use app\common\service\SkuService; use PhpOffice\PhpSpreadsheet\IOFactory; +use PhpOffice\PhpWord\IOFactory as WordIOFactory; use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; use support\Db; use support\Request; use support\Response; +use ZipArchive; class SkuController extends BaseController { @@ -87,17 +89,23 @@ class SkuController extends BaseController public function import(Request $request) { + $file = $request->file('file'); if (!$file) return json(['code'=>1,'msg'=>'未上传文件']); - $path = runtime_path('upload/' . uniqid() . '.xlsx'); - $file->move($path); + $zipPath = runtime_path('upload/' . uniqid() . '.zip'); + $file->move($zipPath); + $extractTo = runtime_path('upload/import/' . uniqid()); + $this->unzipPackage($zipPath,$extractTo); + @unlink($zipPath); + + $path = runtime_path('upload/import/product.xlsx'); $spreadsheet = IOFactory::load($path); $sheet = $spreadsheet->getActiveSheet(); // 提取图片(按行号) - $rowImages = $this->extractImages($sheet); + //$rowImages = $this->extractImages($sheet); $highestRow = $sheet->getHighestRow(); $highestCol = $sheet->getHighestColumn(); @@ -111,7 +119,7 @@ class SkuController extends BaseController 'brand' => '品牌', 'category_id' => '分类', 'price' => '价格', -// 'spec_json' => '商品属性' + //'spec_json' => '商品属性' ]; $fieldsName = array_flip($allowFields); @@ -119,6 +127,7 @@ class SkuController extends BaseController $now = time(); Db::beginTransaction(); + $uploadService = new UploadService(); try { for ($row = 2; $row <= $highestRow; $row++) { $rowData = $sheet->rangeToArray( @@ -137,6 +146,8 @@ class SkuController extends BaseController } $data['category_id'] = PostCategory::where('name', $rowData[3])->value('id')??0; + + //商品属性 if(!empty($rowData[5])){ $speci = explode(';', $rowData[5]); $spec = []; @@ -146,31 +157,43 @@ class SkuController extends BaseController continue; } $attr = ['attr_name'=>$item[0],'attr_value'=>$item[1]]; - $skuAttribute = SkuAttribute::where('attr_name', $item[0])->first(); + $skuAttribute = SkuAttribute::where('attr_name', $item[0]) + ->where('category_id', $data['category_id'])->first(); if (empty($skuAttribute)) { continue; } $attr['attr_id'] = $skuAttribute->id; - $attr['category_id'] = $skuAttribute->category_id; + $attr['category_id'] = $data['category_id']; $spec[] = $attr; $data['spec_json'] = json_encode($spec); } } + //主图 + if(!empty($rowData[6])){ + $tmpPath = runtime_path('upload/import/main_pic/'.$rowData[6]); + $res = $uploadService->uploadLocalFile(['path' => $tmpPath, 'scene' => 'sku']); + $data['main_image'] = $res['file_url']; + } + //详情 + if(!empty($rowData[7])){ + $docxFullPath = runtime_path('upload/import/docs/'.$rowData[7]); + $data['desc'] =$this->docxToHtmlWithImages($docxFullPath); + } + $data['code'] = $this->service->generateUniqueCode(); $data['brand'] = $data['brand'] ?: '西门子'; $data['price'] = $data['price'] ?: 0; $data['status'] = $data['status'] ?? 2; // 图片 - $data['main_image'] = $rowImages[$row] ?? null; + //$data['main_image'] = $rowImages[$row] ?? null; $data['created_at'] = $now; $data['updated_at'] = $now; $insertData[] = $data; - var_dump($insertData); if (count($insertData) >= 200) { Sku::insert($insertData); // Db::table('sku')->insert($insertData); @@ -194,6 +217,88 @@ class SkuController extends BaseController return $this->success([], '导入成功'); } + /** + * 解压 zip 文件 + * + * @param string $zipPath zip 文件绝对路径 + * @param string $extractTo 解压目录 + * @return bool + */ + function unzipPackage(string $zipPath, string $extractTo): bool + { + if (!file_exists($zipPath)) { + throw new \Exception('ZIP 文件不存在'); + } + + if (!is_dir($extractTo)) { + mkdir($extractTo, 0755, true); + } + + $zip = new ZipArchive(); + if ($zip->open($zipPath) !== true) { + throw new \Exception('ZIP 文件打开失败'); + } + + $zip->extractTo($extractTo); + $zip->close(); + + return true; + } + + /** + * docx 转 HTML(自动上传图片到 OSS) + * + * @param string $docxPath docx 文件绝对路径 + * @return string HTML + */ + function docxToHtmlWithImages(string $docxPath): string + { + if (!is_file($docxPath)) { + return ''; + } + + // 1. 读取 docx + $phpWord = WordIOFactory::load($docxPath); + + // 2. 输出 HTML 到临时文件 + $tmpHtml = runtime_path('tmp/' . uniqid() . '.html'); + if (!is_dir(dirname($tmpHtml))) { + mkdir(dirname($tmpHtml), 0755, true); + } + + $writer = WordIOFactory::createWriter($phpWord, 'HTML'); + $writer->save($tmpHtml); + + $html = file_get_contents($tmpHtml); + + $uploadService = new UploadService(); + // 3. 处理 HTML 中的图片 + $html = preg_replace_callback( + '/]+src="([^"]+)"[^>]*>/i', + function ($matches) use ($tmpHtml,$uploadService) { + $src = $matches[1]; + + // docx 导出的图片一般是相对路径 + if (!str_starts_with($src, 'http')) { + $localImg = dirname($tmpHtml) . '/' . $src; + if (is_file($localImg)) { + // 上传到 OSS + $ossUrl = $uploadService->uploadLocalFile(['path' => $localImg, 'scene' => 'desc']); + return str_replace($src, $ossUrl, $matches[0]); + } + } + + return $matches[0]; + }, + $html + ); + + // 清理临时文件 + @unlink($tmpHtml); + + return $html; + } + private function extractImages($sheet): array { $images = []; diff --git a/composer.json b/composer.json index cf009b8..6d18988 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,8 @@ "webman/cache": "^2.1", "shopwwi/webman-filesystem": "1.1.0", "shopwwi/filesystem-oss": "^1.0", - "phpoffice/phpspreadsheet": "^5.3" + "phpoffice/phpspreadsheet": "^5.3", + "phpoffice/phpword": "^1.3" }, "suggest": { "ext-event": "For better performance. " diff --git a/composer.lock b/composer.lock index 4adce4d..103452a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "39297daade796e36307a44b11984f21e", + "content-hash": "ef72614b6d91b3731b2f744070fbe9cd", "packages": [ { "name": "aliyuncs/oss-sdk-php", @@ -2722,6 +2722,64 @@ ], "time": "2025-08-20T22:55:33+00:00" }, + { + "name": "phpoffice/math", + "version": "0.2.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/Math.git", + "reference": "fc2eb6d1a61b058d5dac77197059db30ee3c8329" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/Math/zipball/fc2eb6d1a61b058d5dac77197059db30ee3c8329", + "reference": "fc2eb6d1a61b058d5dac77197059db30ee3c8329", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-dom": "*", + "ext-xml": "*", + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.88 || ^1.0.0", + "phpunit/phpunit": "^7.0 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\Math\\": "src/Math/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Progi1984", + "homepage": "https://lefevre.dev" + } + ], + "description": "Math - Manipulate Math Formula", + "homepage": "https://phpoffice.github.io/Math/", + "keywords": [ + "MathML", + "officemathml", + "php" + ], + "support": { + "issues": "https://github.com/PHPOffice/Math/issues", + "source": "https://github.com/PHPOffice/Math/tree/0.2.0" + }, + "time": "2024-08-12T07:30:45+00:00" + }, { "name": "phpoffice/phpspreadsheet", "version": "5.3.0", @@ -2828,6 +2886,121 @@ }, "time": "2025-11-24T15:47:10+00:00" }, + { + "name": "phpoffice/phpword", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PHPWord.git", + "reference": "8392134ce4b5dba65130ba956231a1602b848b7f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/8392134ce4b5dba65130ba956231a1602b848b7f", + "reference": "8392134ce4b5dba65130ba956231a1602b848b7f", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-xml": "*", + "php": "^7.1|^8.0", + "phpoffice/math": "^0.2" + }, + "require-dev": { + "dompdf/dompdf": "^2.0", + "ext-gd": "*", + "ext-libxml": "*", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.3", + "mpdf/mpdf": "^8.1", + "phpmd/phpmd": "^2.13", + "phpstan/phpstan-phpunit": "@stable", + "phpunit/phpunit": ">=7.0", + "symfony/process": "^4.4 || ^5.0", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Allows writing PDF", + "ext-gd2": "Allows adding images", + "ext-xmlwriter": "Allows writing OOXML and ODF", + "ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template", + "ext-zip": "Allows writing OOXML and ODF" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpWord\\": "src/PhpWord" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "authors": [ + { + "name": "Mark Baker" + }, + { + "name": "Gabriel Bull", + "email": "me@gabrielbull.com", + "homepage": "http://gabrielbull.com/" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net/blog/" + }, + { + "name": "Ivan Lanin", + "homepage": "http://ivan.lanin.org" + }, + { + "name": "Roman Syroeshko", + "homepage": "http://ru.linkedin.com/pub/roman-syroeshko/34/a53/994/" + }, + { + "name": "Antoine de Troostembergh" + } + ], + "description": "PHPWord - A pure PHP library for reading and writing word processing documents (OOXML, ODF, RTF, HTML, PDF)", + "homepage": "https://phpoffice.github.io/PHPWord/", + "keywords": [ + "ISO IEC 29500", + "OOXML", + "Office Open XML", + "OpenDocument", + "OpenXML", + "PhpOffice", + "PhpWord", + "Rich Text Format", + "WordprocessingML", + "doc", + "docx", + "html", + "odf", + "odt", + "office", + "pdf", + "php", + "reader", + "rtf", + "template", + "template processor", + "word", + "writer" + ], + "support": { + "issues": "https://github.com/PHPOffice/PHPWord/issues", + "source": "https://github.com/PHPOffice/PHPWord/tree/1.3.0" + }, + "time": "2024-08-30T18:03:42+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.4", @@ -4490,23 +4663,22 @@ }, { "name": "symfony/http-foundation", - "version": "v7.3.4", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "c061c7c18918b1b64268771aad04b40be41dd2e6" + "reference": "bd1af1e425811d6f077db240c3a588bdb405cd27" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/c061c7c18918b1b64268771aad04b40be41dd2e6", - "reference": "c061c7c18918b1b64268771aad04b40be41dd2e6", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/bd1af1e425811d6f077db240c3a588bdb405cd27", + "reference": "bd1af1e425811d6f077db240c3a588bdb405cd27", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php83": "^1.27" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" }, "conflict": { "doctrine/dbal": "<3.6", @@ -4515,13 +4687,13 @@ "require-dev": { "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.4.12|^7.1.5", - "symfony/clock": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0" + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4549,7 +4721,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.4" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.1" }, "funding": [ { @@ -4569,7 +4741,7 @@ "type": "tidelift" } ], - "time": "2025-09-16T08:38:17+00:00" + "time": "2025-12-07T11:13:10+00:00" }, { "name": "symfony/http-kernel",