From f21eb13cc841f1c71fe45a720a1f4c1147f9129a Mon Sep 17 00:00:00 2001 From: liapples Date: Mon, 20 Dec 2021 01:00:41 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=A7=E5=93=81=E5=AF=BC=E5=85=A5=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E5=88=B0excel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Actions/Grid/ExportToExcelButton.php | 59 ++ .../Actions/Grid/ImportFromExcelButton.php | 59 ++ .../Controllers/ProductController.php | 23 +- .../Extensions/ProductToExcelExporter.php | 18 + app/AdminSupplier/Forms/ExcelImport.php | 52 ++ app/Exports/ProductExport.php | 100 ++++ app/Imports/ProductImport.php | 176 ++++++ app/Imports/ProductSpecImport.php | 63 +++ app/Jobs/ExportProductToExcel.php | 45 ++ app/Jobs/ImportProductFromExcel.php | 63 +++ composer.json | 4 +- composer.lock | 520 +++++++++++++++++- config/excel.php | 328 +++++++++++ tests/Feature/ExampleTest.php | 16 +- 14 files changed, 1514 insertions(+), 12 deletions(-) create mode 100644 app/AdminSupplier/Actions/Grid/ExportToExcelButton.php create mode 100644 app/AdminSupplier/Actions/Grid/ImportFromExcelButton.php create mode 100644 app/AdminSupplier/Extensions/ProductToExcelExporter.php create mode 100644 app/AdminSupplier/Forms/ExcelImport.php create mode 100644 app/Exports/ProductExport.php create mode 100644 app/Imports/ProductImport.php create mode 100644 app/Imports/ProductSpecImport.php create mode 100644 app/Jobs/ExportProductToExcel.php create mode 100644 app/Jobs/ImportProductFromExcel.php create mode 100644 config/excel.php diff --git a/app/AdminSupplier/Actions/Grid/ExportToExcelButton.php b/app/AdminSupplier/Actions/Grid/ExportToExcelButton.php new file mode 100644 index 0000000..6facc6f --- /dev/null +++ b/app/AdminSupplier/Actions/Grid/ExportToExcelButton.php @@ -0,0 +1,59 @@ +appendHtmlAttribute('class', 'btn btn-primary btn-outline'); + + return <<formatHtmlAttributes()}>{$this->title()} +HTML; + } + + public function handle(Request $request) + { + ExportProductToExcel::dispatch(\Admin::user()->id); + return $this->response()->success('导出成功,稍后到导出列表下载'); + } + + /** + * @return string|array|void + */ + public function confirm() + { + // return ['Confirm?', 'contents']; + } + + /** + * @param Model|Authenticatable|HasPermissions|null $user + * + * @return bool + */ + protected function authorize($user): bool + { + return true; + } + + /** + * @return array + */ + protected function parameters() + { + return []; + } +} diff --git a/app/AdminSupplier/Actions/Grid/ImportFromExcelButton.php b/app/AdminSupplier/Actions/Grid/ImportFromExcelButton.php new file mode 100644 index 0000000..ce75a06 --- /dev/null +++ b/app/AdminSupplier/Actions/Grid/ImportFromExcelButton.php @@ -0,0 +1,59 @@ +appendHtmlAttribute('class', 'btn btn-primary btn-outline'); + + return <<formatHtmlAttributes()}>{$this->title()} +HTML; + } + + /** + * @return string|void + */ + protected function href() + { + return admin_url('product/import'); + } + + /** + * @return string|array|void + */ + public function confirm() + { + // return ['Confirm?', 'contents']; + } + + /** + * @param Model|Authenticatable|HasPermissions|null $user + * + * @return bool + */ + protected function authorize($user): bool + { + return true; + } + + /** + * @return array + */ + protected function parameters() + { + return []; + } +} diff --git a/app/AdminSupplier/Controllers/ProductController.php b/app/AdminSupplier/Controllers/ProductController.php index f68f7ee..2aa88e0 100644 --- a/app/AdminSupplier/Controllers/ProductController.php +++ b/app/AdminSupplier/Controllers/ProductController.php @@ -2,28 +2,37 @@ namespace App\AdminSupplier\Controllers; +use App\AdminSupplier\Actions\Grid\ExportToExcelButton; +use App\AdminSupplier\Actions\Grid\ImportFromExcelButton; +use App\AdminSupplier\Extensions\Form\Spec; +use App\AdminSupplier\Forms\ExcelImport; use App\AdminSupplier\Repositories\Product; use App\Common\OrderStatus; use App\Common\ProductStatus; use App\Jobs\ProductSpecSync; -use App\Models\AgentProduct; -use App\Models\AgentProductItem; use App\Models\AgentProductSpec; use App\Models\Category; use App\Models\DiyForm; use App\Models\Order; -use App\Models\ProductSpec; use Dcat\Admin\Admin; use Dcat\Admin\Form; use Dcat\Admin\Form\NestedForm; use Dcat\Admin\Grid; +use Dcat\Admin\Layout\Content; use Dcat\Admin\Show; use Dcat\Admin\Http\Controllers\AdminController; +use Dcat\Admin\Widgets\Card; class ProductController extends AdminController { protected $title = '产品'; - /** + + public function import(Content $content): Content + { + return $content->title('从Excel导入产品')->body(new Card(new ExcelImport())); + } + + /** * Make a grid builder. * * @return Grid @@ -33,6 +42,10 @@ class ProductController extends AdminController return Grid::make(new Product(['category:id,name']), function (Grid $grid) { $grid->model()->where('supplier_id', Admin::user()->id); + $grid->tools(new ImportFromExcelButton()); + $grid->tools(new ExportToExcelButton()); +// $grid->export(new ProductToExcelExporter); + $category_id = request()->input('cid'); if ($category_id) { $grid->model()->whereIn('category_id', [$category_id, ...$this->get_category_child_ids($category_id)]); @@ -139,7 +152,9 @@ class ProductController extends AdminController protected function form() { Admin::user()->publish_type = json_decode(Admin::user()->publish_type, true); +// Form::extend('spec', Spec::class); return Form::make(new Product(['spec']), function (Form $form) { +// $form->spec('spec', '产品规格')->required(); //不允许编辑非自己数据 if ($form->isEditing() && $form->model()->supplier_id != Admin::user()->id) { return $form->response()->error('数据不存在'); diff --git a/app/AdminSupplier/Extensions/ProductToExcelExporter.php b/app/AdminSupplier/Extensions/ProductToExcelExporter.php new file mode 100644 index 0000000..ad6bc0e --- /dev/null +++ b/app/AdminSupplier/Extensions/ProductToExcelExporter.php @@ -0,0 +1,18 @@ +id), '导出产品-' . date('Y-m-d H:i:s') . '.xlsx')->send(); + } +} diff --git a/app/AdminSupplier/Forms/ExcelImport.php b/app/AdminSupplier/Forms/ExcelImport.php new file mode 100644 index 0000000..51cd410 --- /dev/null +++ b/app/AdminSupplier/Forms/ExcelImport.php @@ -0,0 +1,52 @@ +path($zip_file)) . '.zip'; + Storage::disk('public')->move($zip_file, $to_path);*/ + ImportProductFromExcel::dispatch(\Admin::user()->id, Storage::disk('public')->path($zip_file)); + } + + return $this->response()->success('操作成功,导入产品可能需要几分钟,请勿重复导入,请耐心等待')->refresh(); + } + + /** + * Build a form here. + */ + public function form() + { + $this->multipleFile('zip_file', 'ZIP文件') + ->help('注意:请上传zip文件,上传文件大小不能超过' . ini_get('upload_max_filesize') . + '。文件导入模板') + ->accept('zip') + ->uniqueName()->autoUpload()->required(); + } + + /** + * The data of the form. + * + * @return array + */ + public function default() + { + return []; + } +} diff --git a/app/Exports/ProductExport.php b/app/Exports/ProductExport.php new file mode 100644 index 0000000..981d68b --- /dev/null +++ b/app/Exports/ProductExport.php @@ -0,0 +1,100 @@ +supplier_id = $supplier_id; + } + + public function query() + { + return Product::with('category:id,name')->where('supplier_id', $this->supplier_id); + } + + public function prepareRows($rows) + { + return $rows->transform(function ($row) { + return [ + 'id' => $row->id, + 'status' => ProductStatus::array()[$row->status] ?? '', + 'category_name' => $row->category?->name ?? '', + 'title' => $row->title, + 'price' => $row->price, + 'original_price' => $row->original_price, + 'stock' => $row->stock, + 'know' => $row->know, + 'content' => $row->content, + 'verify_mobile' => $row->verify_mobile, + 'diy_form_id' => $row->diy_form_id, + '出发地' => $row->extends['field_0_departure_place'] ?? '', + '出发地经度' => $row->extends['field_0_departure_place_longitude'] ?? '', + '出发地纬度' => $row->extends['field_0_departure_place_latitude'] ?? '', + '目的地' => $row->extends['field_0_destination'] ?? '', + '目的地经度' => $row->extends['field_0_destination_longitude'] ?? '', + '目的地纬度' => $row->extends['field_0_destination_latitude'] ?? '', + '行程起始时间' => $row->extends['field_0_date?->start'] ?? '', + '行程结束时间' => $row->extends['field_0_date?->end'] ?? '', + '酒店名' => $row->extends['field_1_name'] ?? '', + '酒店地址' => $row->extends['field_1_address'] ?? '', + '酒店经度' => $row->extends['field_1_longitude'] ?? '', + '酒店纬度' => $row->extends['field_1_latitude'] ?? '', + '景区名' => $row->extends['field_2_name'] ?? '', + '景区地址' => $row->extends['field_2_address'] ?? '', + '景区经度' => $row->extends['field_2_longitude'] ?? '', + '景区纬度' => $row->extends['field_2_latitude'] ?? '', + '餐厅名' => $row->extends['field_3_name'] ?? '', + '餐厅地址' => $row->extends['field_3_address'] ?? '', + '餐厅经度' => $row->extends['field_3_longitude'] ?? '', + '餐厅纬度' => $row->extends['field_3_latitude'] ?? '', + '交通地址' => $row->extends['field_4_address'] ?? '', + '交通经度' => $row->extends['field_4_longitude'] ?? '', + '交通纬度' => $row->extends['field_4_latitude'] ?? '', + '购物地址' => $row->extends['field_5_address'] ?? '', + '购物经度' => $row->extends['field_5_longitude'] ?? '', + '购物纬度' => $row->extends['field_5_latitude'] ?? '', + ]; + }); + } + + public function map($row): array + { + return [ + $row->id, + $row->category->name, + + ]; + } + + public function headings(): array + { + return [ + 'ID', '状态', '分类', '产品标题', '销售价', '市场价', '库存', '旅客须知', '产品详情', '核销手机号', '信息收集表单ID', + '出发地', '出发地经度', '出发地纬度', '目的地', '目的地经度', '目的地纬度', '行程起始时间', '行程结束时间', + '酒店名', '酒店地址', '酒店经度', '酒店纬度', + '景区名', '景区地址', '景区经度', '景区纬度', + '餐厅名', '餐厅地址', '餐厅经度', '餐厅纬度', + '交通地址', '交通经度', '交通纬度', + '购物地址', '购物经度', '购物纬度', + ]; + } + + public function columnFormats(): array + { + for ($i = 65; $i <= 90; $i++) { + $format[chr($i)] = NumberFormat::FORMAT_TEXT; + } + return $format; + } +} diff --git a/app/Imports/ProductImport.php b/app/Imports/ProductImport.php new file mode 100644 index 0000000..a7dc5c2 --- /dev/null +++ b/app/Imports/ProductImport.php @@ -0,0 +1,176 @@ +supplier_id = $supplier_id; + $this->extract_path = $extract_path; + } + + public function collection(Collection $collection) + { + if ($collection->isEmpty()) { + return; + } + + //校验数组count()是否正确 + if (count($collection[0]) < 35) { + throw new \Exception('Excel产品信息格式不正确'); + } + + $this->keys = array_flip($collection[0]->toArray()); + + //去除第一行的标题 + unset($collection[0]); + + //$collection + $this->createData($collection); + } + + private function createData($rows) + { + $keys = $this->keys; + foreach ($rows as $product_index => $row) { + $category = Category::where(['agent_id' => 0, 'name' => trim($row[$keys['分类']])])->first(); + if (!$category) continue; + + $insert_data = [ + 'supplier_id' => $this->supplier_id, + 'category_id' => $category->id, + 'type' => $category->publish_type, + 'title' => $row[$keys['产品标题']] ?? '', + 'price' => $row[$keys['销售价']] ?? 0, + 'original_price' => $row[$keys['市场价']] ?? 0, + 'stock' => $row[$keys['库存']] ?? 0, + 'know' => $row[$keys['旅客须知']] ?? '', + 'content' => $row[$keys['产品详情']] ?? '', + 'verify_mobile' => $row[$keys['核销手机号']] ?? '', + 'diy_form_id' => $row[$keys['信息收集表单ID']] ?? '', + 'pictures' => $this->get_pictures($product_index), + ]; + + # 扩展字段 + $insert_data['extends'] = $this->get_extends($category->publish_type, $row); + + if ($category->publish_type == 0) { + $insert_data['longitude'] = $insert_data['extends']['field_0_departure_place_longitude'] ?? 0; + $insert_data['latitude'] = $insert_data['extends']['field_0_departure_place_latitude'] ?? 0; + $insert_data['address'] = $insert_data['extends']['field_0_departure_place'] ?? ''; + } else { + $insert_data['longitude'] = $insert_data['extends']['field_'.$category->publish_type.'_longitude'] ?? 0; + $insert_data['latitude'] = $insert_data['extends']['field_'.$category->publish_type.'_latitude'] ?? 0; + $insert_data['address'] = $insert_data['extends']['field_'.$category->publish_type.'_address'] ?? ''; + } + + $product = Product::create($insert_data); + if ($product->id) { + $this->insert_spec($product_index, $product->id); + } + } + } + + # 插入规格 + private function insert_spec($product_index, $product_id) + { + $spec_file = $this->extract_path . "/产品规格{$product_index}.xlsx"; + if (file_exists($spec_file)) { + Excel::import(new ProductSpecImport($product_id), $spec_file); + } + } + + # 遍历图片文件,并移动到storage/app/public/supplier/import目录下 + private function get_pictures($product_index): array + { + $storage_disk = Storage::disk('public'); + + $http_path = 'supplier/import/' . $this->supplier_id . '/' . date('Y-m'); + + if (!$storage_disk->exists($http_path)) { + $storage_disk->makeDirectory($http_path); //Storage的makeDirectory才能递归创建 + } + + $image_path = $this->extract_path . "/产品主图/$product_index"; + chdir($image_path); + + $pictures = []; + foreach (glob('*') as $file) { + $ext = strtolower(pathinfo($file)['extension']); + if (!in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'tiff', 'jfif'])) { + continue; + } + + $filename = '/' . md5_file($file) . '.' . $ext; + $move_file = realpath($file); + + # Storage::move只适用于storage/app目录下的文件,且是相对路径 + if (rename($move_file, $storage_disk->path($http_path . $filename))) { + $pictures[] = $http_path . $filename; + } + } + return $pictures; + } + + # 获取扩展字段 + private function get_extends(int $publish_type, $row): array + { + $keys = $this->keys; + return match ($publish_type) { + 0 => [ + 'field_0_departure_place' => $row[$keys['出发地']] ?? '', + 'field_0_departure_place_longitude' => $row[$keys['出发地经度']] ?? 0, + 'field_0_departure_place_latitude' => $row[$keys['出发地纬度']] ?? 0, + 'field_0_destination' => $row[$keys['目的地']] ?? '', + 'field_0_destination_longitude' => $row[$keys['目的地经度']] ?? 0, + 'field_0_destination_latitude' => $row[$keys['目的地纬度']] ?? 0, + 'field_0_date' => [ + # Excel日期是从1900-01-01起,PHP日期是1970-01-01起,所以要减去25569天数得到正确日期 + 'start' => !empty($row[$keys['行程起始时间']]) ? date('Y-m-d', strtotime('+ ' . ($row[$keys['行程起始时间']] - 25569) . 'day', 0)) : '', + 'end' => !empty($row[$keys['行程结束时间']]) ? date('Y-m-d', strtotime('+ ' . ($row[$keys['行程结束时间']] - 25569) . 'day', 0)) : '', + ], + ], + 1 => [ + 'field_1_name' => $row[$keys['酒店名']] ?? '', + 'field_1_address' => $row[$keys['酒店地址']] ?? '', + 'field_1_longitude' => $row[$keys['酒店经度']] ?? 0, + 'field_1_latitude' => $row[$keys['酒店纬度']] ?? 0, + ], + 2 => [ + 'field_2_name' => $row[$keys['景区名']] ?? '', + 'field_2_address' => $row[$keys['景区地址']] ?? '', + 'field_2_longitude' => $row[$keys['景区经度']] ?? 0, + 'field_2_latitude' => $row[$keys['景区纬度']] ?? 0, + ], + 3 => [ + 'field_3_name' => $row[$keys['餐厅名']] ?? '', + 'field_3_address' => $row[$keys['餐厅地址']] ?? '', + 'field_3_longitude' => $row[$keys['餐厅经度']] ?? 0, + 'field_3_latitude' => $row[$keys['餐厅纬度']] ?? 0, + ], + 4 => [ + 'field_4_address' => $row[$keys['交通地址']] ?? '', + 'field_4_longitude' => $row[$keys['交通经度']] ?? 0, + 'field_4_latitude' => $row[$keys['交通纬度']] ?? 0, + ], + 5 => [ + 'field_5_address' => $row[$keys['购物地址']] ?? '', + 'field_5_longitude' => $row[$keys['购物经度']] ?? 0, + 'field_5_latitude' => $row[$keys['购物纬度']] ?? 0, + ], + default => [], + }; + } +} diff --git a/app/Imports/ProductSpecImport.php b/app/Imports/ProductSpecImport.php new file mode 100644 index 0000000..05f8c38 --- /dev/null +++ b/app/Imports/ProductSpecImport.php @@ -0,0 +1,63 @@ +product_id = $product_id; + } + + public function collection(Collection $collection) + { + if ($collection->isEmpty()) { + return; + } + + //校验数组count()是否正确 + if (count($collection[0]) < 6) { + throw new \Exception('Excel产品规格格式不正确'); + } + + $this->keys = array_flip($collection[0]->toArray()); + + //去除第一行的标题 + unset($collection[0]); + + $this->createData($collection); + } + + private function createData($rows) + { + $keys = $this->keys; + DB::beginTransaction(); + foreach ($rows as $row) { + # Excel日期是从1900-01-01起的天数,PHP日期是1970-01-01起,所以要减去25569天数得到正确日期 + $row[$keys['日期']] = date('Y-m-d', strtotime('+ ' . ($row[$keys['日期']] - 25569) . 'day', 0)); + + ProductSpec::updateOrCreate([ + 'product_id' => $this->product_id, + 'name' => $row[$keys['规格名称']], + 'date' => $row[$keys['日期']], + ], [ + 'product_id' => $this->product_id, + 'name' => $row[$keys['规格名称']], + 'date' => $row[$keys['日期']], + 'stock' => $row[$keys['库存']], + 'original_price' => $row[$keys['市场价']], + 'price' => $row[$keys['销售价']], + 'cost_price' => $row[$keys['成本价']], + ]); + } + DB::commit(); + } +} diff --git a/app/Jobs/ExportProductToExcel.php b/app/Jobs/ExportProductToExcel.php new file mode 100644 index 0000000..1633d23 --- /dev/null +++ b/app/Jobs/ExportProductToExcel.php @@ -0,0 +1,45 @@ +supplier_id = $supplier_id; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $filePath = "supplier/export/{$this->supplier_id}/" . date('Y-m') . "/导出产品-" . date('Y-m-d_H-i-s') . '.xlsx'; + Excel::store(new ProductExport($this->supplier_id), $filePath, 'public'); + ProductExportLog::create([ + 'supplier_id' => $this->supplier_id, + 'filename' => $filePath, + ]); + } +} diff --git a/app/Jobs/ImportProductFromExcel.php b/app/Jobs/ImportProductFromExcel.php new file mode 100644 index 0000000..bd7c40a --- /dev/null +++ b/app/Jobs/ImportProductFromExcel.php @@ -0,0 +1,63 @@ +supplier_id = $supplier_id; + $this->zip_file = $zip_file; + } + + /** + * Execute the job. + * + * @return void + * @throws Exception + */ + public function handle() + { + if (!$this->supplier_id) { + throw new Exception('未指定要导入的供应商'); + } else if (!$this->zip_file) { + throw new Exception('未指定要导入的zip文件'); + } + + $extract_path = Storage::path('excel/extract/' . basename($this->zip_file, '.zip')); + + $zip = new ZipArchive; + if ($zip->open($this->zip_file) === TRUE && $zip->extractTo($extract_path)) { + $zip->close(); + } else { + throw new Exception("解压文件 {$this->zip_file} 失败!"); + } + + # 解压后删除压缩文件 + unlink($this->zip_file); + + Excel::import(new ProductImport($this->supplier_id, $extract_path), $extract_path . '/产品.xlsx'); + } +} diff --git a/composer.json b/composer.json index 011e3bb..1de5328 100644 --- a/composer.json +++ b/composer.json @@ -12,10 +12,12 @@ "guzzlehttp/guzzle": "^7.0.1", "laravel/framework": "^8.40", "laravel/tinker": "^2.5", + "maatwebsite/excel": "^3.1", "overtrue/wechat": "~5.0", "sentry/sentry-laravel": "^2.8", "super-eggs/dcat-distpicker": "^2.0", - "tencentcloud/tencentcloud-sdk-php": "^3.0" + "tencentcloud/tencentcloud-sdk-php": "^3.0", + "ext-zip": "*" }, "require-dev": { "facade/ignition": "^2.5", diff --git a/composer.lock b/composer.lock index 0236747..426deea 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": "53437ceb8091fc69e865f45c680e7d34", + "content-hash": "696ba1a4ceafd58323ab85db5ed92218", "packages": [ { "name": "asm89/stack-cors", @@ -1192,6 +1192,66 @@ ], "time": "2020-12-29T14:50:06+00:00" }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.13.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/08e27c97e4c6ed02f37c5b2b20488046c8d90d75", + "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=5.2" + }, + "require-dev": { + "simpletest/simpletest": "dev-master#72de02a7b80c6bb8864ef9bf66d41d2f58f826bd" + }, + "type": "library", + "autoload": { + "psr-0": { + "HTMLPurifier": "library/" + }, + "files": [ + "library/HTMLPurifier.composer.php" + ], + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/master" + }, + "time": "2020-06-29T00:56:53+00:00" + }, { "name": "fideloper/proxy", "version": "4.4.1", @@ -2463,6 +2523,286 @@ ], "time": "2021-01-18T20:58:21+00:00" }, + { + "name": "maatwebsite/excel", + "version": "3.1.34", + "source": { + "type": "git", + "url": "https://github.com/SpartnerNL/Laravel-Excel.git", + "reference": "d7446f0e808d83be128835c4b403c9e4a65b20f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/d7446f0e808d83be128835c4b403c9e4a65b20f3", + "reference": "d7446f0e808d83be128835c4b403c9e4a65b20f3", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-json": "*", + "illuminate/support": "5.8.*|^6.0|^7.0|^8.0", + "php": "^7.0|^8.0", + "phpoffice/phpspreadsheet": "^1.18" + }, + "require-dev": { + "orchestra/testbench": "^6.0", + "predis/predis": "^1.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Maatwebsite\\Excel\\ExcelServiceProvider" + ], + "aliases": { + "Excel": "Maatwebsite\\Excel\\Facades\\Excel" + } + } + }, + "autoload": { + "psr-4": { + "Maatwebsite\\Excel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Patrick Brouwers", + "email": "patrick@spartner.nl" + } + ], + "description": "Supercharged Excel exports and imports in Laravel", + "keywords": [ + "PHPExcel", + "batch", + "csv", + "excel", + "export", + "import", + "laravel", + "php", + "phpspreadsheet" + ], + "support": { + "issues": "https://github.com/SpartnerNL/Laravel-Excel/issues", + "source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.34" + }, + "funding": [ + { + "url": "https://laravel-excel.com/commercial-support", + "type": "custom" + }, + { + "url": "https://github.com/patrickbrouwers", + "type": "github" + } + ], + "time": "2021-12-02T16:17:16+00:00" + }, + { + "name": "maennchen/zipstream-php", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "c4c5803cc1f93df3d2448478ef79394a5981cc58" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/c4c5803cc1f93df3d2448478ef79394a5981cc58", + "reference": "c4c5803cc1f93df3d2448478ef79394a5981cc58", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "myclabs/php-enum": "^1.5", + "php": ">= 7.1", + "psr/http-message": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "require-dev": { + "ext-zip": "*", + "guzzlehttp/guzzle": ">= 6.3", + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": ">= 7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jonatan Männchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "András Kolesár", + "email": "kolesar@kolesar.hu" + } + ], + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": [ + "stream", + "zip" + ], + "support": { + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/master" + }, + "funding": [ + { + "url": "https://opencollective.com/zipstream", + "type": "open_collective" + } + ], + "time": "2020-05-30T13:11:16+00:00" + }, + { + "name": "markbaker/complex", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "ab8bc271e404909db09ff2d5ffa1e538085c0f22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/ab8bc271e404909db09ff2d5ffa1e538085c0f22", + "reference": "ab8bc271e404909db09ff2d5ffa1e538085c0f22", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "phpcompatibility/php-compatibility": "^9.0", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.3", + "squizlabs/php_codesniffer": "^3.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Complex\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" + } + ], + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", + "keywords": [ + "complex", + "mathematics" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.1" + }, + "time": "2021-06-29T15:32:53+00:00" + }, + { + "name": "markbaker/matrix", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "c66aefcafb4f6c269510e9ac46b82619a904c576" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/c66aefcafb4f6c269510e9ac46b82619a904c576", + "reference": "c66aefcafb4f6c269510e9ac46b82619a904c576", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "phpcompatibility/php-compatibility": "^9.0", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.3", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matrix\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@demon-angel.eu" + } + ], + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.0" + }, + "time": "2021-07-01T19:01:15+00:00" + }, { "name": "mockery/mockery", "version": "1.4.3", @@ -2643,6 +2983,72 @@ ], "time": "2021-07-23T07:42:52+00:00" }, + { + "name": "myclabs/php-enum", + "version": "1.8.3", + "source": { + "type": "git", + "url": "https://github.com/myclabs/php-enum.git", + "reference": "b942d263c641ddb5190929ff840c68f78713e937" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/php-enum/zipball/b942d263c641ddb5190929ff840c68f78713e937", + "reference": "b942d263c641ddb5190929ff840c68f78713e937", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-json": "*", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "1.*", + "vimeo/psalm": "^4.6.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "MyCLabs\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP Enum contributors", + "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" + } + ], + "description": "PHP Enum implementation", + "homepage": "http://github.com/myclabs/php-enum", + "keywords": [ + "enum" + ], + "support": { + "issues": "https://github.com/myclabs/php-enum/issues", + "source": "https://github.com/myclabs/php-enum/tree/1.8.3" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum", + "type": "tidelift" + } + ], + "time": "2021-07-05T08:18:36+00:00" + }, { "name": "nesbot/carbon", "version": "2.53.1", @@ -3721,6 +4127,116 @@ }, "time": "2020-07-07T09:29:14+00:00" }, + { + "name": "phpoffice/phpspreadsheet", + "version": "1.20.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "44436f270bb134b4a94670f3d020a85dfa0a3c02" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/44436f270bb134b4a94670f3d020a85dfa0a3c02", + "reference": "44436f270bb134b4a94670f3d020a85dfa0a3c02", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "ezyang/htmlpurifier": "^4.13", + "maennchen/zipstream-php": "^2.1", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": "^7.3 || ^8.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/simple-cache": "^1.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "dompdf/dompdf": "^1.0", + "friendsofphp/php-cs-fixer": "^3.2", + "jpgraph/jpgraph": "^4.0", + "mpdf/mpdf": "^8.0", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^8.5 || ^9.0", + "squizlabs/php_codesniffer": "^3.6", + "tecnickcom/tcpdf": "^6.4" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer (doesn't yet support PHP8)", + "jpgraph/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer (doesn't yet support PHP8)" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + } + ], + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "keywords": [ + "OpenXML", + "excel", + "gnumeric", + "ods", + "php", + "spreadsheet", + "xls", + "xlsx" + ], + "support": { + "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.20.0" + }, + "time": "2021-11-23T15:23:42+00:00" + }, { "name": "phpoption/phpoption", "version": "1.8.0", @@ -11179,7 +11695,7 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^7.4|^8.0" + "php": "^8.0" }, "platform-dev": [], "plugin-api-version": "2.1.0" diff --git a/config/excel.php b/config/excel.php new file mode 100644 index 0000000..d22ab6a --- /dev/null +++ b/config/excel.php @@ -0,0 +1,328 @@ + [ + + /* + |-------------------------------------------------------------------------- + | Chunk size + |-------------------------------------------------------------------------- + | + | When using FromQuery, the query is automatically chunked. + | Here you can specify how big the chunk should be. + | + */ + 'chunk_size' => 1000, + + /* + |-------------------------------------------------------------------------- + | Pre-calculate formulas during export + |-------------------------------------------------------------------------- + */ + 'pre_calculate_formulas' => false, + + /* + |-------------------------------------------------------------------------- + | Enable strict null comparison + |-------------------------------------------------------------------------- + | + | When enabling strict null comparison empty cells ('') will + | be added to the sheet. + */ + 'strict_null_comparison' => false, + + /* + |-------------------------------------------------------------------------- + | CSV Settings + |-------------------------------------------------------------------------- + | + | Configure e.g. delimiter, enclosure and line ending for CSV exports. + | + */ + 'csv' => [ + 'delimiter' => ',', + 'enclosure' => '"', + 'line_ending' => PHP_EOL, + 'use_bom' => false, + 'include_separator_line' => false, + 'excel_compatibility' => false, + ], + + /* + |-------------------------------------------------------------------------- + | Worksheet properties + |-------------------------------------------------------------------------- + | + | Configure e.g. default title, creator, subject,... + | + */ + 'properties' => [ + 'creator' => '', + 'lastModifiedBy' => '', + 'title' => '', + 'description' => '', + 'subject' => '', + 'keywords' => '', + 'category' => '', + 'manager' => '', + 'company' => '', + ], + ], + + 'imports' => [ + + /* + |-------------------------------------------------------------------------- + | Read Only + |-------------------------------------------------------------------------- + | + | When dealing with imports, you might only be interested in the + | data that the sheet exists. By default we ignore all styles, + | however if you want to do some logic based on style data + | you can enable it by setting read_only to false. + | + */ + 'read_only' => true, + + /* + |-------------------------------------------------------------------------- + | Ignore Empty + |-------------------------------------------------------------------------- + | + | When dealing with imports, you might be interested in ignoring + | rows that have null values or empty strings. By default rows + | containing empty strings or empty values are not ignored but can be + | ignored by enabling the setting ignore_empty to true. + | + */ + 'ignore_empty' => false, + + /* + |-------------------------------------------------------------------------- + | Heading Row Formatter + |-------------------------------------------------------------------------- + | + | Configure the heading row formatter. + | Available options: none|slug|custom + | + */ + 'heading_row' => [ + 'formatter' => 'slug', + ], + + /* + |-------------------------------------------------------------------------- + | CSV Settings + |-------------------------------------------------------------------------- + | + | Configure e.g. delimiter, enclosure and line ending for CSV imports. + | + */ + 'csv' => [ + 'delimiter' => ',', + 'enclosure' => '"', + 'escape_character' => '\\', + 'contiguous' => false, + 'input_encoding' => 'UTF-8', + ], + + /* + |-------------------------------------------------------------------------- + | Worksheet properties + |-------------------------------------------------------------------------- + | + | Configure e.g. default title, creator, subject,... + | + */ + 'properties' => [ + 'creator' => '', + 'lastModifiedBy' => '', + 'title' => '', + 'description' => '', + 'subject' => '', + 'keywords' => '', + 'category' => '', + 'manager' => '', + 'company' => '', + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Extension detector + |-------------------------------------------------------------------------- + | + | Configure here which writer/reader type should be used when the package + | needs to guess the correct type based on the extension alone. + | + */ + 'extension_detector' => [ + 'xlsx' => Excel::XLSX, + 'xlsm' => Excel::XLSX, + 'xltx' => Excel::XLSX, + 'xltm' => Excel::XLSX, + 'xls' => Excel::XLS, + 'xlt' => Excel::XLS, + 'ods' => Excel::ODS, + 'ots' => Excel::ODS, + 'slk' => Excel::SLK, + 'xml' => Excel::XML, + 'gnumeric' => Excel::GNUMERIC, + 'htm' => Excel::HTML, + 'html' => Excel::HTML, + 'csv' => Excel::CSV, + 'tsv' => Excel::TSV, + + /* + |-------------------------------------------------------------------------- + | PDF Extension + |-------------------------------------------------------------------------- + | + | Configure here which Pdf driver should be used by default. + | Available options: Excel::MPDF | Excel::TCPDF | Excel::DOMPDF + | + */ + 'pdf' => Excel::DOMPDF, + ], + + /* + |-------------------------------------------------------------------------- + | Value Binder + |-------------------------------------------------------------------------- + | + | PhpSpreadsheet offers a way to hook into the process of a value being + | written to a cell. In there some assumptions are made on how the + | value should be formatted. If you want to change those defaults, + | you can implement your own default value binder. + | + | Possible value binders: + | + | [x] Maatwebsite\Excel\DefaultValueBinder::class + | [x] PhpOffice\PhpSpreadsheet\Cell\StringValueBinder::class + | [x] PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder::class + | + */ + 'value_binder' => [ + 'default' => Maatwebsite\Excel\DefaultValueBinder::class, + ], + + 'cache' => [ + /* + |-------------------------------------------------------------------------- + | Default cell caching driver + |-------------------------------------------------------------------------- + | + | By default PhpSpreadsheet keeps all cell values in memory, however when + | dealing with large files, this might result into memory issues. If you + | want to mitigate that, you can configure a cell caching driver here. + | When using the illuminate driver, it will store each value in a the + | cache store. This can slow down the process, because it needs to + | store each value. You can use the "batch" store if you want to + | only persist to the store when the memory limit is reached. + | + | Drivers: memory|illuminate|batch + | + */ + 'driver' => 'memory', + + /* + |-------------------------------------------------------------------------- + | Batch memory caching + |-------------------------------------------------------------------------- + | + | When dealing with the "batch" caching driver, it will only + | persist to the store when the memory limit is reached. + | Here you can tweak the memory limit to your liking. + | + */ + 'batch' => [ + 'memory_limit' => 60000, + ], + + /* + |-------------------------------------------------------------------------- + | Illuminate cache + |-------------------------------------------------------------------------- + | + | When using the "illuminate" caching driver, it will automatically use + | your default cache store. However if you prefer to have the cell + | cache on a separate store, you can configure the store name here. + | You can use any store defined in your cache config. When leaving + | at "null" it will use the default store. + | + */ + 'illuminate' => [ + 'store' => null, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Transaction Handler + |-------------------------------------------------------------------------- + | + | By default the import is wrapped in a transaction. This is useful + | for when an import may fail and you want to retry it. With the + | transactions, the previous import gets rolled-back. + | + | You can disable the transaction handler by setting this to null. + | Or you can choose a custom made transaction handler here. + | + | Supported handlers: null|db + | + */ + 'transactions' => [ + 'handler' => 'db', + ], + + 'temporary_files' => [ + + /* + |-------------------------------------------------------------------------- + | Local Temporary Path + |-------------------------------------------------------------------------- + | + | When exporting and importing files, we use a temporary file, before + | storing reading or downloading. Here you can customize that path. + | + */ + 'local_path' => storage_path('framework/cache/laravel-excel'), + + /* + |-------------------------------------------------------------------------- + | Remote Temporary Disk + |-------------------------------------------------------------------------- + | + | When dealing with a multi server setup with queues in which you + | cannot rely on having a shared local temporary path, you might + | want to store the temporary file on a shared disk. During the + | queue executing, we'll retrieve the temporary file from that + | location instead. When left to null, it will always use + | the local path. This setting only has effect when using + | in conjunction with queued imports and exports. + | + */ + 'remote_disk' => null, + 'remote_prefix' => null, + + /* + |-------------------------------------------------------------------------- + | Force Resync + |-------------------------------------------------------------------------- + | + | When dealing with a multi server setup as above, it's possible + | for the clean up that occurs after entire queue has been run to only + | cleanup the server that the last AfterImportJob runs on. The rest of the server + | would still have the local temporary file stored on it. In this case your + | local storage limits can be exceeded and future imports won't be processed. + | To mitigate this you can set this config value to be true, so that after every + | queued chunk is processed the local temporary file is deleted on the server that + | processed it. + | + */ + 'force_resync_remote' => null, + ], +]; diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php index 4ae02bc..6bef536 100644 --- a/tests/Feature/ExampleTest.php +++ b/tests/Feature/ExampleTest.php @@ -2,20 +2,26 @@ namespace Tests\Feature; -use Illuminate\Foundation\Testing\RefreshDatabase; +use App\Exports\ProductExport; +use App\Imports\ProductSpecImport; +use App\Jobs\ExportProductToExcel; +use Illuminate\Support\Facades\Storage; +use Maatwebsite\Excel\Facades\Excel; +use PhpOffice\PhpSpreadsheet\Shared\Date; use Tests\TestCase; class ExampleTest extends TestCase { - /** + private string $extract_path = 'G:\WEB\vhosts\hainan.com\storage\app\excel/extract/2_import'; + private int $supplier_id = 2; + + /** * A basic test example. * * @return void */ public function test_example() { - $response = $this->get('/'); - - $response->assertStatus(200); + ExportProductToExcel::dispatch($this->supplier_id); } }