14 changed files with 1514 additions and 12 deletions
-
59app/AdminSupplier/Actions/Grid/ExportToExcelButton.php
-
59app/AdminSupplier/Actions/Grid/ImportFromExcelButton.php
-
23app/AdminSupplier/Controllers/ProductController.php
-
18app/AdminSupplier/Extensions/ProductToExcelExporter.php
-
52app/AdminSupplier/Forms/ExcelImport.php
-
100app/Exports/ProductExport.php
-
176app/Imports/ProductImport.php
-
63app/Imports/ProductSpecImport.php
-
45app/Jobs/ExportProductToExcel.php
-
63app/Jobs/ImportProductFromExcel.php
-
4composer.json
-
520composer.lock
-
328config/excel.php
-
16tests/Feature/ExampleTest.php
@ -0,0 +1,59 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\AdminSupplier\Actions\Grid; |
||||
|
|
||||
|
use App\Jobs\ExportProductToExcel; |
||||
|
use Dcat\Admin\Grid\Tools\AbstractTool; |
||||
|
use Dcat\Admin\Traits\HasPermissions; |
||||
|
use Illuminate\Contracts\Auth\Authenticatable; |
||||
|
use Illuminate\Database\Eloquent\Model; |
||||
|
use Illuminate\Http\Request; |
||||
|
|
||||
|
class ExportToExcelButton extends AbstractTool |
||||
|
{ |
||||
|
/** |
||||
|
* @return string |
||||
|
*/ |
||||
|
protected $title = '导入所有产品到excel'; |
||||
|
|
||||
|
public function html() |
||||
|
{ |
||||
|
$this->appendHtmlAttribute('class', 'btn btn-primary btn-outline'); |
||||
|
|
||||
|
return <<<HTML |
||||
|
<button {$this->formatHtmlAttributes()}>{$this->title()}</button> |
||||
|
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 []; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,59 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\AdminSupplier\Actions\Grid; |
||||
|
|
||||
|
use Dcat\Admin\Grid\Tools\AbstractTool; |
||||
|
use Dcat\Admin\Traits\HasPermissions; |
||||
|
use Illuminate\Contracts\Auth\Authenticatable; |
||||
|
use Illuminate\Database\Eloquent\Model; |
||||
|
|
||||
|
class ImportFromExcelButton extends AbstractTool |
||||
|
{ |
||||
|
/** |
||||
|
* @return string |
||||
|
*/ |
||||
|
protected $title = '从Excel导入'; |
||||
|
|
||||
|
public function html() |
||||
|
{ |
||||
|
$this->appendHtmlAttribute('class', 'btn btn-primary btn-outline'); |
||||
|
|
||||
|
return <<<HTML |
||||
|
<a {$this->formatHtmlAttributes()}>{$this->title()}</a> |
||||
|
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 []; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\AdminSupplier\Extensions; |
||||
|
use App\Exports\ProductExport; |
||||
|
use Dcat\Admin\Admin; |
||||
|
use Dcat\Admin\Grid\Exporters\AbstractExporter; |
||||
|
use Maatwebsite\Excel\Facades\Excel; |
||||
|
|
||||
|
/** |
||||
|
* Excel导出功能 |
||||
|
*/ |
||||
|
class ProductToExcelExporter extends AbstractExporter |
||||
|
{ |
||||
|
public function export() |
||||
|
{ |
||||
|
return Excel::download(new ProductExport(Admin::user()->id), '导出产品-' . date('Y-m-d H:i:s') . '.xlsx')->send(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,52 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\AdminSupplier\Forms; |
||||
|
|
||||
|
use App\Jobs\ImportProductFromExcel; |
||||
|
use Dcat\Admin\Widgets\Form; |
||||
|
use Illuminate\Support\Facades\Storage; |
||||
|
|
||||
|
class ExcelImport extends Form |
||||
|
{ |
||||
|
/** |
||||
|
* Handle the form request. |
||||
|
* |
||||
|
* @param array $input |
||||
|
* |
||||
|
* @return mixed |
||||
|
*/ |
||||
|
public function handle(array $input) |
||||
|
{ |
||||
|
foreach ($input['zip_file'] as $zip_file) { |
||||
|
/*# 使用md5_file来重命名文件,保证唯一
|
||||
|
$to_path = str_replace(basename($zip_file), '', $zip_file); |
||||
|
$to_path .= md5_file(Storage::disk('public')->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') . |
||||
|
'。<a href="' . Storage::disk('public')->url('import.zip') . '" target="_blank">文件导入模板</a>') |
||||
|
->accept('zip') |
||||
|
->uniqueName()->autoUpload()->required(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* The data of the form. |
||||
|
* |
||||
|
* @return array |
||||
|
*/ |
||||
|
public function default() |
||||
|
{ |
||||
|
return []; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,100 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Exports; |
||||
|
|
||||
|
use App\Common\ProductStatus; |
||||
|
use App\Models\Product; |
||||
|
use Maatwebsite\Excel\Concerns\FromQuery; |
||||
|
use Maatwebsite\Excel\Concerns\WithColumnFormatting; |
||||
|
use Maatwebsite\Excel\Concerns\WithHeadings; |
||||
|
use PhpOffice\PhpSpreadsheet\Style\NumberFormat; |
||||
|
|
||||
|
class ProductExport implements FromQuery, WithHeadings, WithColumnFormatting |
||||
|
{ |
||||
|
private int $supplier_id; |
||||
|
|
||||
|
public function __construct(int $supplier_id) |
||||
|
{ |
||||
|
$this->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; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,176 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Imports; |
||||
|
|
||||
|
use App\Models\Category; |
||||
|
use App\Models\Product; |
||||
|
use Illuminate\Support\Collection; |
||||
|
use Illuminate\Support\Facades\Storage; |
||||
|
use Maatwebsite\Excel\Concerns\ToCollection; |
||||
|
use Maatwebsite\Excel\Facades\Excel; |
||||
|
|
||||
|
class ProductImport implements ToCollection |
||||
|
{ |
||||
|
private int $supplier_id; |
||||
|
private string $extract_path; |
||||
|
private array $keys = []; |
||||
|
|
||||
|
public function __construct(int $supplier_id, $extract_path) |
||||
|
{ |
||||
|
$this->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 => [], |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Imports; |
||||
|
|
||||
|
use App\Models\ProductSpec; |
||||
|
use Illuminate\Support\Collection; |
||||
|
use Illuminate\Support\Facades\DB; |
||||
|
use Maatwebsite\Excel\Concerns\ToCollection; |
||||
|
|
||||
|
class ProductSpecImport implements ToCollection |
||||
|
{ |
||||
|
private int $product_id; |
||||
|
private array $keys; |
||||
|
|
||||
|
public function __construct(int $product_id) |
||||
|
{ |
||||
|
$this->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(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Jobs; |
||||
|
|
||||
|
use App\Exports\ProductExport; |
||||
|
use App\Models\ProductExportLog; |
||||
|
use Illuminate\Bus\Queueable; |
||||
|
use Illuminate\Contracts\Queue\ShouldBeUnique; |
||||
|
use Illuminate\Contracts\Queue\ShouldQueue; |
||||
|
use Illuminate\Foundation\Bus\Dispatchable; |
||||
|
use Illuminate\Queue\InteractsWithQueue; |
||||
|
use Illuminate\Queue\SerializesModels; |
||||
|
use Maatwebsite\Excel\Facades\Excel; |
||||
|
|
||||
|
class ExportProductToExcel implements ShouldQueue |
||||
|
{ |
||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; |
||||
|
|
||||
|
private int $supplier_id; |
||||
|
|
||||
|
/** |
||||
|
* Create a new job instance. |
||||
|
* |
||||
|
* @return void |
||||
|
*/ |
||||
|
public function __construct(int $supplier_id) |
||||
|
{ |
||||
|
$this->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, |
||||
|
]); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Jobs; |
||||
|
|
||||
|
use App\Imports\ProductImport; |
||||
|
use Exception; |
||||
|
use Illuminate\Bus\Queueable; |
||||
|
use Illuminate\Contracts\Queue\ShouldBeUnique; |
||||
|
use Illuminate\Contracts\Queue\ShouldQueue; |
||||
|
use Illuminate\Foundation\Bus\Dispatchable; |
||||
|
use Illuminate\Queue\InteractsWithQueue; |
||||
|
use Illuminate\Queue\SerializesModels; |
||||
|
use Illuminate\Support\Facades\Storage; |
||||
|
use Maatwebsite\Excel\Facades\Excel; |
||||
|
use ZipArchive; |
||||
|
|
||||
|
class ImportProductFromExcel implements ShouldQueue |
||||
|
{ |
||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; |
||||
|
|
||||
|
private int $supplier_id; |
||||
|
private string $zip_file; |
||||
|
|
||||
|
/** |
||||
|
* Create a new job instance. |
||||
|
* |
||||
|
* @return void |
||||
|
*/ |
||||
|
public function __construct(int $supplier_id, string $zip_file) |
||||
|
{ |
||||
|
$this->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'); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,328 @@ |
|||||
|
<?php |
||||
|
|
||||
|
use Maatwebsite\Excel\Excel; |
||||
|
|
||||
|
return [ |
||||
|
'exports' => [ |
||||
|
|
||||
|
/* |
||||
|
|-------------------------------------------------------------------------- |
||||
|
| 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, |
||||
|
], |
||||
|
]; |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue