laravel+xlswriter实现百万数据导出

前提

使用xlswriter进行改造

<?php

namespace App\Exports;

use Throwable;
use Carbon\Carbon;
use Vtiful\Kernel\Excel;
use App\Models\ExportTask;
use App\Models\SalesPerformance;
use Illuminate\Bus\Queueable;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\Storage;
use Illuminate\Contracts\Queue\ShouldQueue;

class SalesPerformanceExport implements ShouldQueue
{
    use Queueable;

    protected ExportTask $task;
    protected array $request;

    /**
     * 存储文件系统配置
     * @var string
     */
    protected string $disk = 'public';

    /**
     * 分批处理数据的大小
     * @var int
     */
    protected int $chunkSize = 1000;

    /**
     * 设置任务超时时间
     * @var int
     */
    public int $timeout = 3600;

    public function __construct(array $request, ExportTask $task)
    {
        $this->task    = $task;
        $this->request = $request;
    }

    public function handle(): void
    {
        try {
            // 更新任务状态为处理中
            $this->task->update([
                'status'     => 'processing',
                'started_at' => now(),
            ]);

            // 初始化 xlswriter
            $excel = new Excel([
                'path' => Storage::disk($this->disk)->path(dirname($this->task->file_path))
            ]);

            // 设置导出文件名
            $excelFile = $excel->constMemory(basename($this->task->file_path), 'Sheet1', false);

            // 设置表头
            $headers = [
                '员工',
                '职位',
                '工作日期',
                '业务类型',
                '接诊类型',
                '顾客姓名',
                '项目/产品名称',
                '应收金额',
                '实收金额',
                '欠款金额',
                '余额支付',
                '服务占比',
                '计提金额',
                '备注'
            ];
            $excelFile->header($headers);

            // 设置列宽
            $excelFile->setColumn('A:A', 13);
            $excelFile->setColumn('B:B', 13);
            $excelFile->setColumn('C:C', 15);
            $excelFile->setColumn('D:D', 15);
            $excelFile->setColumn('E:E', 10);
            $excelFile->setColumn('F:F', 20);
            $excelFile->setColumn('G:G', 40);
            $excelFile->setColumn('N:N', 60);

            // 写入数据
            $query      = $this->getQuery();
            $position   = config('setting.sales_performance.position');
            $table_name = config('setting.sales_performance.table_name');

            // 分批处理数据并直接写入
            $query->chunk($this->chunkSize, function ($records) use ($excelFile, $position, $table_name) {
                $batchData = [];
                foreach ($records as $row) {
                    $batchData[] = [
                        $row->user_name,
                        $position[$row->position] ?? '',
                        Carbon::parse($row->created_at)->toDateString(),
                        $table_name[$row->table_name] ?? '',
                        get_reception_type_name($row->reception_type),
                        $row->customer_name,
                        $row->product_name . $row->goods_name,
                        $row->payable,
                        $row->income,
                        $row->arrearage,
                        $row->deposit,
                        $row->rate,
                        $row->amount,
                        $row->remark,
                    ];
                }
                // 每一批数据直接写入文件
                if (!empty($batchData)) {
                    $excelFile->data($batchData);
                }
            });

            // 关闭文件
            $excelFile->output();

            // 更新任务状态为完成
            $this->task->update([
                'status'       => 'completed',
                'completed_at' => now(),
            ]);

        } catch (Throwable $exception) {
            $this->task->update([
                'status'        => 'failed',
                'failed_at'     => now(),
                'error_message' => $exception->getMessage(),
            ]);
        }
    }

    protected function getQuery()
    {
        return SalesPerformance::query()
            ->select([
                'users.name as user_name',
                'sales_performance.position',
                'sales_performance.created_at',
                'sales_performance.table_name',
                'sales_performance.reception_type',
                'customer.name as customer_name',
                'sales_performance.product_name',
                'sales_performance.goods_name',
                'sales_performance.payable',
                'sales_performance.income',
                'sales_performance.arrearage',
                'sales_performance.deposit',
                'sales_performance.rate',
                'sales_performance.amount',
                'sales_performance.remark',
            ])
            ->leftJoin('users', 'users.id', '=', 'sales_performance.user_id')
            ->leftJoin('customer', 'customer.id', '=', 'sales_performance.customer_id')
            ->when($keyword = $this->request['keyword'] ?? null, fn(Builder $query) => $query->where('customer.keyword', 'like', "%{$keyword}%"))
            ->whereBetween('sales_performance.created_at', $this->request['created_at'])
            ->queryConditions('ReportPerformanceSales', $this->request['filters'] ?? [])
            ->orderBy('sales_performance.created_at', 'desc');
    }

    /**
     * 任务失败时调用
     * @param Throwable $exception
     * @return void
     */
    public function failed(Throwable $exception): void
    {
        $this->task->update([
            'status'        => 'failed',
            'failed_at'     => now(),
            'error_message' => '导出任务执行失败: ' . $exception->getMessage(),
        ]);
    }
}

实测

  • 目前最多跑了50W+数据 耗时10分钟
本作品采用《CC 协议》,转载必须注明作者和本文链接
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
讨论数量: 4

用了多少 内存

14小时前 评论
91it (楼主) 13小时前
不负岁月 (作者) 13小时前
91it (楼主) 12小时前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!