指尖上的记忆指尖上的记忆
首页
  • 基础
  • Laravel框架
  • Symfony框架
  • 基础
  • Gin框架
  • 基础
  • Spring框架
  • 命令
  • Nginx
  • Ai
  • Deploy
  • Docker
  • K8s
  • Micro
  • RabbitMQ
  • Mysql
  • PostgreSsql
  • Redis
  • MongoDb
  • Html
  • Js
  • 前端
  • 后端
  • Git
  • 知识扫盲
  • Golang
🌟 gitHub
首页
  • 基础
  • Laravel框架
  • Symfony框架
  • 基础
  • Gin框架
  • 基础
  • Spring框架
  • 命令
  • Nginx
  • Ai
  • Deploy
  • Docker
  • K8s
  • Micro
  • RabbitMQ
  • Mysql
  • PostgreSsql
  • Redis
  • MongoDb
  • Html
  • Js
  • 前端
  • 后端
  • Git
  • 知识扫盲
  • Golang
🌟 gitHub

在开发过程中经常会用到 excel导入导出数据,以下是几个场景以及对应的解决方案,操作之前先安装依赖: composer require maatwebsite/excel (最好3.1+版本): 1.excel批量导入

在控制器里添加如下方法:
    public function import(Request $request, ProductImport $productImport)
    {

        try {
                    $productImport->import($request->file('excel'));
                    //Excel::import(new ProductImport(), $request->file('excel')); //也可以这样使用
            } catch (\Throwable $throwable) {
                    return $this->_error($throwable->getMessage());
            }

            return $this->_success("导入成功");
    }

在 namespace App\Imports; 下定义 ProductImport:
<?php

namespace App\Imports;

use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Maatwebsite\Excel\Concerns\ToCollection;
use Maatwebsite\Excel\Concerns\WithChunkReading;
use Maatwebsite\Excel\Concerns\Importable;

class ProductImport implements ToCollection, WithChunkReading
{
    use Importable;

    public function chunkSize(): int
    {
        //控制数据读取大小,防止一次导入太多数据
        return 500;
    }

    public function collection(Collection $rows)
    {
        //如果需要去除表头
        unset($rows[0]);

        foreach ($rows as $key => $row) {
            $this->_saveData($row->toArray());
        }
    }

    private function _saveData($data)
    {
        try {
            DB::transaction(function () use ($data) {
                $product['name']         = $data[0] ?? '';
                $product['cover_image']  = $data[1] ?? '';
                $product['price']        = $data[2] ?? 0;
                $product['description']  = $data[3] ?? '';
                $product['detail']       = $data[4] ?? '';

                $exist = Product::query()->where(['name' => $data[0]])->first();
                if (!$exist) {
                    Product::query()->create($product);
                }
            });

        } catch (\Throwable $exception) {
            Log::info("create error is:" . $exception->getMessage());
        }
    }
}

2.批量导出 ①数据量不大的时候可以有下面几种方法


1.基于数组的导出
在控制器下添加如下方法
public function exports(Request $request)
    {
        $data = ProductService::index($request->all());

        //可以传递 $request->input('page', 1) 分页参数
        return (new ProductExport($data))->download();
    }

在 namespace App\Imports;下添加如下 ProductExport 类

use Illuminate\Contracts\Support\Responsable;
use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\FromArray;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Events\AfterSheet;

class ProductExport implements FromArray, Responsable, WithMapping, WithHeadings, ShouldAutoSize, WithEvents
{
    use Exportable;

    protected $data;
    protected $fileName = '.xlsx';

    public function __construct(array $data)
    {
        $this->data     = $data;
        $title          = '产品数据-' . date('Y-m-d');
        $this->fileName = $title . '.xlsx';

        return $this;
    }

    public function array(): array
    {
        return $this->data;
    }

    public function map($product): array
    {
        return [
            $product['name'],
            $product['cover_image'],
            $product['price'],
            $product['description'],
            $product['detail']
        ];
    }

    public function headings(): array
    {
        return [
            '名称',
            '产品图片',
            '产品价格',
            '产品描述',
            '产品详情'
        ];
    }

    public function registerEvents(): array
    {
        return [
            AfterSheet::class => function (AfterSheet $event) {
                $event->sheet->getColumnDimension('A')->setAutoSize(false)->setWidth(10);
                $event->sheet->getColumnDimension('B')->setAutoSize(false)->setWidth(50);
                $event->sheet->getColumnDimension('C')->setAutoSize(false)->setWidth(10);
                $event->sheet->getColumnDimension('D')->setAutoSize(false)->setWidth(10);
                $event->sheet->getColumnDimension('E')->setAutoSize(false)->setWidth(10);
            }
        ];
    }
}

//当需要导出图片的时候可以按下面的方法操作
<?php
/**
 * Created by PhpStorm.
 * User: guoshipeng
 * Date: 2022/9/20
 * Time: 15:25
 */

namespace App\Imports;

use Illuminate\Contracts\Support\Responsable;
use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\FromArray;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Events\AfterSheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;

class ProductExport implements FromArray, Responsable, WithMapping, WithHeadings, ShouldAutoSize, WithEvents
{
    use Exportable;

    protected $data;
    protected $fileName = '.xlsx';


    public function __construct(array $data, $page = 1)
    {
        $this->data     = $data;
        $title          = '用户数据-' . '第' . $page . '页-' . date('Y-m-d');//这个是导出当前页的写法
        $this->fileName = $title . '.xlsx';

        return $this;
    }

    public function array(): array
    {
        return $this->data;
    }

    public function map($user): array
    {
        return [
            $user->id,
            $user->name,
            $user->description,
            //$user->cover_image, //这个图片链接可以不用了 ,因为后面会通过 Drawing 将链接渲染为图片
        ];
    }

    public function headings(): array
    {
        return [
            '产品ID',
            '产品名称',
            '产品详情',
            '产品图片',
        ];
    }

    public function registerEvents(): array
    {
        return [
            AfterSheet::class => function (AfterSheet $event) {
                $event->sheet->getColumnDimension('A')->setAutoSize(false)->setWidth(10);
                $event->sheet->getColumnDimension('B')->setAutoSize(false)->setWidth(50);
                $event->sheet->getColumnDimension('C')->setAutoSize(false)->setWidth(30);
                $event->sheet->getColumnDimension('D')->setAutoSize(false)->setWidth(10);

                //导出图片部分,官网:https://docs.laravel-excel.com/3.1/exports/drawings.html 中通过实现 implements WithDrawings 里的方法实现也可以,但是具体到逻辑,不用 implements WithDrawings,直接使用Drawing类实例化对象即可,我认为下面的写法更合理一些
                $data = $this->array();
                foreach ($data as $key => $val) {
                    $temp = $val->toArray();
                    $event->sheet->getRowDimension($key + 2)->setRowHeight(30);

                    //直接用var_dump($temp['id']),没有任何效果,但是通过Log打印日志,就可以取到值,说明是有值的
                    //Log::info("id is:".$temp['id']);
                    //Log::info("cover_image is:".$temp['cover_image']);

                    if ($temp['cover_image']) {
                        $base_name = pathinfo($temp['cover_image'])['basename'];

                        if (!file_exists('/img/' . $base_name)) {//防止重复存储图片
                            $get_file = @file_get_contents($temp['cover_image']);//需要通过 file_get_contents 而不是 file_put_contents
                            if ($get_file) {
                                $fp = @fopen(storage_path('/img/' . $base_name), "w");
                                @fwrite($fp, $get_file);
                                @fclose($fp);
                            }
                        }

                        $drawing = new Drawing(); // 给每个图片链接一个 Drawing 对象处理
                        $drawing->setResizeProportional(true);// 这个还没搞懂怎么使用,感觉是要重置属性
                        //在导出图片的时候不能设置width,一设置就会有展示问题,比如这里设置为10,实际上展示的根本就不是10,而是很大的一个值
                        //$drawing->setWidth(10);
                        $drawing->setPath(storage_path('/img/' . $base_name)); // 这个必须为本地图片,需要先将外网图片下载存储到本地,我这里存储到storage/img下,将来这个文件夹肯定会很大,尽管我做了不重复存储的判断
                        $drawing->setHeight(30);// 这里要设置 一个宽度,不然显示也会有问题,但是 尽管这里设置了30
                        $drawing->setCoordinates('D' . ($key + 2));// 这个主要用来设置 图片展示在哪行哪列
                        $drawing->setWorksheet($event->sheet->getDelegate());// 当前 $drawing 作用在哪里
                    }
                }

            }
        ];
    }
}

2.普通查询导出
在控制器下添加如下方法:
public function export()
    {
        return (new ProductExport())->download();
    }
在 namespace App\Imports; 下添加如下代码:
<?php
/**
 * Created by PhpStorm.
 * User: guoshipeng
 * Date: 2022/9/30
 * Time: 10:09
 */

namespace App\Imports;

use App\Models\Product\Product;
use Illuminate\Contracts\Queue\ShouldQueue;
use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\FromQuery;
use Maatwebsite\Excel\Concerns\WithCustomQuerySize;
use Maatwebsite\Excel\Concerns\WithMapping;

/**
 *
 * Class ProductExport
 * @package App\Imports
 */
class ProductExport implements FromQuery, WithMapping
{
    use Exportable;
    protected $where;
    protected $fileName = '产品数据测试.xlsx';

    public function query()
    {
        return Product::query()->where('id', '<', 100); // 使用 where 查询,这里只用查询,不用get;这个where 条件可以在控制器里实例化 ProductExport 的时候通过自定义set/get方法传递,或者在构造方法里传递
    }

    public function map($product): array
    {
        return [
            $product->id,
            $product->name,
            $product->description,
        ];
    }
}

3.分多个excel sheet的导出
在控制器下添加如下方法:
    public function export(Request $request)
    {
        $sheetData = [];
        Product::query()->chunkById(1000, function ($items) use (&$sheetData) {

            $data          = [];
            $sheetName     = '产品列表数据';//每一个sheet的名称,多个的话 excel 会自动在后面追加阿拉伯数字
            $columnFormats = [];//根据需求添加
            $temp          = [];
            if ($items->isNotEmpty()) {

                $items->each(function ($item) use (&$temp) {
                    $temp[] = ['id' => $item->id, 'name' => $item->name, 'cover_image' => $item->cover_image, 'price' => $item->price, 'description' => $item->description, 'created_at' => $item->created_at];
                });
            }

            //对列进行特殊处理,use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
            $columnFormats['D'] = NumberFormat::FORMAT_NUMBER_0; //格式化数字
            $columnFormats['F'] = NumberFormat::FORMAT_DATE_YYYYMMDD2; //格式化时间

            $data['sheet']         = $sheetName;
            $data['data']          = $temp;
            $data['columnFormats'] = $columnFormats;

            $sheetData[] = $data;

        });

        // 标题
        $headings = ['ID', '名称', '封面图', '价格', '描述', '创建时间'];
        $sheet    = [];
        foreach ($sheetData as &$item) {
            $sheet[] = new ProductMultiSheet($item['data'], $headings, $item['sheet'], $item['columnFormats']);
        }

        return (new MultiTest())->download($sheet);
    }

在 namespace App\Imports;下添加如下代码,由于是分多个sheet导出,需要两个类来操作,一个做导出操作,还有一个做单个excel表数据处理:

<?php
/**
 * Created by PhpStorm.
 * User: guoshipeng
 * Date: 2022/9/30
 * Time: 11:48
 */

namespace App\Imports;

use Maatwebsite\Excel\Concerns\FromArray;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithTitle;
use Maatwebsite\Excel\Concerns\WithColumnFormatting;
use Maatwebsite\Excel\Events\AfterSheet;

class ProductMultiSheet implements FromArray, WithTitle, WithHeadings, WithColumnFormatting, WithEvents
{
    private $data;
    private $headings;
    private $title;
    private $columnFormats;

    /**
     * @param array $data 数据
     * @param array $headings 头部
     * @param string $title sheet名称
     * @param array $columnFormats 格式化列
     */
    public function __construct(array $data, array $headings, string $title, array $columnFormats = []) {
        $this->data          = $data;
        $this->headings      = $headings;
        $this->title         = $title;
        $this->columnFormats = $columnFormats;
    }

    /**
     * 数据
     * @return array
     */
    public function array() : array {
        return $this->data ? : [];
    }

    /**
     * 表头
     * @return array
     */
    public function headings() : array {
        return $this->headings ? : [];
    }

    /**
     * sheet名称
     * @return string
     */
    public function title() : string {
        return $this->title ? : '';
    }

    /**
     * 列格式列
     * @return array
     */
    public function columnFormats() : array {
        return $this->columnFormats ? : [];
    }

    public function registerEvents(): array
    {
        return [
            AfterSheet::class => function (AfterSheet $event) {
                $event->sheet->getColumnDimension('A')->setAutoSize(false)->setWidth(10);
                $event->sheet->getColumnDimension('B')->setAutoSize(false)->setWidth(50);
                $event->sheet->getColumnDimension('C')->setAutoSize(false)->setWidth(10);
                $event->sheet->getColumnDimension('D')->setAutoSize(false)->setWidth(10);
                $event->sheet->getColumnDimension('E')->setAutoSize(false)->setWidth(10);
                $event->sheet->getColumnDimension('F')->setAutoSize(false)->setWidth(10);
            }
        ];
    }
}


<?php
/**
 * Created by PhpStorm.
 * User: guoshipeng
 * Date: 2022/9/30
 * Time: 13:55
 */

namespace App\Imports;


use Maatwebsite\Excel\Concerns\WithMultipleSheets;

class MultiTest implements WithMultipleSheets
{
    private $sheets;

    /**
     * @param array $data
     * @param array $registerEvents
     * @param string $filename
     * @return \Symfony\Component\HttpFoundation\BinaryFileResponse
     */
    public function download(array $sheets, string $filename = '产品数据.xlsx')
    {
        $this->sheets = $sheets;
        return \Maatwebsite\Excel\Facades\Excel::download($this, $filename);
    }

    /**
     * @return array
     */
    public function sheets(): array
    {
        return $this->sheets ?: [];
    }
}

本来还想在多个sheet导出,通过异步队列实现,但是没有成功,这个以后再弄

②数据量很大的时候需要用到异步队列(redis)操作

在控制器下添加如下方法:
public function export()
    {
         //注意,通过队列导出 需要先创建 store 里的 filePath 文件地址(完整地址)
        //注意,这个异步执行可能需要很久,所以 打开时间太快的话 产品数据测试2.xlsx 可能没有数据
        //注意,这个 chain 里的job 必须要通过队列执行(implements ShouldQueue),不能同步执行,我感觉是 store 走了队列,然后 job 也要走队列
        (new ProductMultiExport())->store(storage_path('logs').'/产品数据测试2.xlsx')->chain([new SendEmail()]);

        return $this->_success("操作完成");
    }

在 namespace App\Imports; 下添加如下代码:
<?php
/**
 * Created by PhpStorm.
 * User: guoshipeng
 * Date: 2022/9/30
 * Time: 10:09
 */

namespace App\Imports;

use App\Models\Product\Product;
use Illuminate\Contracts\Queue\ShouldQueue;
use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\FromQuery;
use Maatwebsite\Excel\Concerns\WithCustomQuerySize;
use Maatwebsite\Excel\Concerns\WithMapping;

/**
 * 可以通过 implements ShouldQueue 实现通过队列导出
 *
 * Class CompanyMultiExport
 * @package App\Imports
 */
class ProductMultiExport implements FromQuery, WithMapping, WithCustomQuerySize, ShouldQueue
{
    use Exportable;
    protected $where;
    protected $fileName = '产品数据测试.xlsx';

    //注意:当使用队列异步导出的时候,好像只能用query()获取数据,array()一直获取不到数据
    public function query()
    {
        return Product::query()->where('id', '<', 10000); // 使用 where 查询,这里只用查询,不用get
    }

    public function map($product): array
    {
        return [
            $product->id,
            $product->name,
            $product->description,
        ];
    }

    //必须要有这个,默认队列只会导出 1000 条数据,需要指定导出总数,不然数据不完整
    public function querySize(): int
    {
        return Product::query()->where('id', '<', 10000)->count();//和query的条件要一样
    }
}

在 namespace App\Jobs; 下添加下面代码:
<?php

/**
 * Created by PhpStorm.
 * User: guoshipeng
 * Date: 2022/9/30
 * Time: 16:10
 */

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Log;

class SendEmail implements ShouldQueue
{
    use Queueable, Dispatchable, InteractsWithQueue, SerializesModels;

    public function handle()
    {
        Log::info("下载完成,可以发邮件了通知下载了");
    }
}