ThinkPHP6文件導(dǎo)入功能開發(fā)指南:兼容Excel與CSV的高效數(shù)據(jù)導(dǎo)入

一、引言

在上篇文章中,我們探討了文件導(dǎo)出的相關(guān)內(nèi)容,而今天,我們將聚焦于文件導(dǎo)入功能的實(shí)現(xiàn)。由于上級(jí)的要求,本次導(dǎo)入操作依然沿用同步方式,而非異步操作。本次導(dǎo)入功能旨在兼容 Excel 和 CSV 兩種格式的文件,以下是詳細(xì)的實(shí)現(xiàn)思路與步驟。

二、功能實(shí)現(xiàn)思路

(一)整體流程

  1. 前端上傳文件 :前端負(fù)責(zé)將需要導(dǎo)入的文件(支持 Excel 和 CSV 格式)上傳至后端。
  2. 后端接收并上傳到服務(wù)器 :后端接收到文件后,將其上傳到服務(wù)器進(jìn)行臨時(shí)存儲(chǔ)。
  3. 讀取文件內(nèi)容 :根據(jù)文件類型(Excel 或 CSV),調(diào)用相應(yīng)的函數(shù)讀取文件內(nèi)容,獲取數(shù)據(jù)。
  4. 存儲(chǔ)到數(shù)據(jù)庫 :將讀取到的數(shù)據(jù)進(jìn)行處理后存儲(chǔ)到數(shù)據(jù)庫中。
  5. 刪除服務(wù)器文件 :為避免服務(wù)器空間被占用,文件讀取和處理完成后,及時(shí)刪除服務(wù)器上的臨時(shí)文件。
  6. 給前端提示 :向前端返回導(dǎo)入結(jié)果,提示導(dǎo)入是否成功。

以下是整體流程圖:

795400f8e324556b6aee4a1c8f83eb0.png

(二)技術(shù)選型

為了方便處理 Excel 文件,我們選擇使用 phpoffice/phpspreadsheet 這一第三方庫,可通過 composer 進(jìn)行安裝,在項(xiàng)目根目錄下執(zhí)行以下命令:

composer require phpoffice/phpspreadsheet

三、技術(shù)實(shí)現(xiàn)細(xì)節(jié)

(一)封裝公共函數(shù)

app/common.php 文件內(nèi),我們封裝了以下幾個(gè)公共函數(shù),用于實(shí)現(xiàn)文件上傳、讀取文件內(nèi)容等功能。

1. 上傳文件并讀取內(nèi)容的公共函數(shù)

if (!function_exists('import_file_to_process_data')) {
    /**
     * @param file $file 用戶上傳的文件對(duì)象
     * @param callable $rowProcessor  批數(shù)據(jù)的處理回調(diào)函數(shù)
     * @param int $batchSize 批處理的數(shù)據(jù)行數(shù),默認(rèn)為1000行
     * @return mixed
     * desc: 導(dǎo)入文件以處理數(shù)據(jù)(該函數(shù)負(fù)責(zé)驗(yàn)證、保存文件,并根據(jù)文件類型(csv、xlsx、xls)調(diào)用相應(yīng)的處理函數(shù)
     *  它使用批處理方式處理文件中的數(shù)據(jù),以提高處理效率)
     * author: lijiwei
     * datetime: 2025/3/5下午4:27
     */
    function import_file_to_process_data($file, $rowProcessor, $batchSize = 1000)
    {
        // 驗(yàn)證文件
        $validate = \think\facade\Validate::rule([
            'file' => 'file|fileExt:csv,xlsx,xls|fileSize:10240000'
        ]);

        if (!$validate->check(['file' => $file])) {
            throw new \think\exception\ValidateException($validate->getError());
        }

        // 保存文件
        $path = \think\facade\Filesystem::disk('public')->putFile('import', $file);
        if (!$path) {
            throw new \think\exception\ValidateException('文件上傳失敗');
        }

        // 獲取文件的完整路徑
        $filePath = \think\facade\Filesystem::disk('public')->path($path);
        if (!$filePath) {
            throw new \think\exception\ValidateException('文件上傳失敗【-1】');
        }

        // 獲取文件擴(kuò)展名字
        $extension = pathinfo($filePath, PATHINFO_EXTENSION);

        try {
            // 檢查文件擴(kuò)展名選擇不同的讀取方式
            if ($extension === 'csv') {
                return read_csv_file($filePath, $rowProcessor, $batchSize);
            } elseif (in_array($extension, ['xlsx', 'xls'])) {
                return read_excel_file($filePath, $rowProcessor, $batchSize);
            } else {
                throw new \think\exception\ValidateException('不支持的文件類型');
            }
        } catch (\Exception $e) {
            throw_exception('導(dǎo)入文件失敗:' . $e->getMessage());
        } finally {
            \think\facade\Filesystem::disk('public')->delete($path);
        }
    }
}

2. 讀取 Excel 文件的函數(shù)

if (!function_exists('read_excel_file')) {
    /**
     * @param string  $filePath Excel文件的路徑
     * @param callable $rowProcessor 處理每一行數(shù)據(jù)的回調(diào)函數(shù)
     * @param int $batchSize 每批處理的行數(shù)
     * @return Generator
     * desc: 讀取Excel文件并按批處理行數(shù)據(jù)(函數(shù)使用生成器模式讀取Excel文件中的數(shù)據(jù),并允許通過$rowProcessor回調(diào)函數(shù)處理每一行數(shù)據(jù)
     *  函數(shù)按批處理數(shù)據(jù),以減少內(nèi)存消耗)
     * author: author
     * datetime: 2025/3/4下午4:54
     */
    function read_excel_file(string $filePath, callable $rowProcessor, int $batchSize)
    {
        // 加載Excel文件
        $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($filePath);
        // 獲取活動(dòng)工作表
        $worksheet = $spreadsheet->getActiveSheet();
        // 獲取最高行號(hào)
        $highestRow = $worksheet->getHighestRow();

        // 初始化當(dāng)前批次和行計(jì)數(shù)器
        $dataBatchs = [];
        $currentBatch = [];
        $rowCount = 0;

        // 跳過表頭,從第二行開始遍歷
        for ($row = 2; $row <= $highestRow; $row++) {
            $rowData = [];
            $columnIndex = 'A';
            // 遍歷當(dāng)前行的每一列,直到遇到空單元格
            while ($worksheet->getCell($columnIndex . $row)->getValue() !== null) {
                $rowData[] = $worksheet->getCell($columnIndex . $row)->getValue();
                $columnIndex++;
            }

            if ($rowData) {
                // 使用$rowProcessor回調(diào)函數(shù)處理當(dāng)前行的數(shù)據(jù)
                $processedRow = call_user_func($rowProcessor, $rowData);
                // 將處理后的行數(shù)據(jù)添加到當(dāng)前批次
                $currentBatch[] = $processedRow;
                $rowCount++;

                // 如果達(dá)到批次大小,則生成當(dāng)前批次的數(shù)據(jù),并重置當(dāng)前批次
                if ($rowCount % $batchSize === 0) {
                    $dataBatchs[] = $currentBatch;
                    $currentBatch = [];
                }
            }
        }

        // 如果還有未處理的批次,則生成剩余批次的數(shù)據(jù)
        if (!empty($currentBatch)) {
            $dataBatchs[] = $currentBatch;
        }

        return $dataBatchs;
    }
}

3. 讀取 CSV 文件的函數(shù)

if (!function_exists('read_csv_file')) {
    /**
     * @param string  $filePath CSV文件的路徑
     * @param callable $rowProcessor 處理每一行的回調(diào)函數(shù)
     * @param int $batchSize 每個(gè)批次包含的行數(shù)
     * @return array|Generator 生成器,每次生成一個(gè)批次的處理結(jié)果
     * desc: 讀取CSV文件并按批次返回處理后的行(本函數(shù)打開指定的CSV文件,并使用提供的行處理器函數(shù)處理每一行當(dāng)處理的行數(shù)達(dá)到指定的批次大小時(shí),
     *  會(huì)生成當(dāng)前批次的處理結(jié)果并清空當(dāng)前批次的緩存,以支持大文件的分批處理)
     * author: author
     * datetime: 2025/3/5下午4:41
     */
    function read_csv_file($filePath, $rowProcessor, $batchSize)
    {
        // 檢查文件是否存在且可讀
        if (!is_readable($filePath)) {
            return [];
        }

        $handle = fopen($filePath, 'r');
        if (!$handle) {
            return [];
        }

        // 處理 BOM 頭(僅當(dāng)文件大小 >=3 字節(jié)時(shí))
        if (filesize($filePath) >= 3) {
            $bom = fread($handle, 3);
            if ($bom !== "\xEF\xBB\xBF") {
                rewind($handle);
            }
        } else {
            rewind($handle);
        }

        // 讀取表頭
        $header = fgetcsv($handle, 0, ',');
        if ($header === false) {
            fclose($handle);
            return [];
        }
        // 循環(huán)讀取數(shù)據(jù)行
        $dataBatchs = [];
        $currentBatch = [];
        $rowCount = 0;
        while (($row = fgetcsv($handle, 0, ',')) !== false) {
            $processedRow = call_user_func($rowProcessor, $row);
            if ($processedRow !== null) {
                $currentBatch[] = $processedRow;
                $rowCount++;

                if ($rowCount % $batchSize === 0) {
                    $dataBatchs[] = $currentBatch;
                    $currentBatch = [];
                }
            }
        }

        // 生成剩余數(shù)據(jù)
        if (!empty($currentBatch)) {
            $dataBatchs[] = $currentBatch;
        }

        fclose($handle);

        return $dataBatchs;
    }
}

(二)接口實(shí)現(xiàn)

1. 控制器層

    /**
     * @return \think\response\Json
     * desc: 導(dǎo)入SKU
     * author: author
     * datetime: 2025/3/5上午10:19
     */
    public function importSKU()
    {
        $file = $this->request->file('file');
        if (empty($file)) throw_exception('請(qǐng)選擇文件');

        SkuService::getInstance()->import_sku($file);
        return success([], '導(dǎo)入成功');
    }

2. Service 層

    /**
     * @param $file
     * @return void
     * desc: 導(dǎo)入SKU信息(本函數(shù)負(fù)責(zé)處理SKU數(shù)據(jù)的導(dǎo)入,包括讀取文件、驗(yàn)證數(shù)據(jù)、數(shù)據(jù)庫事務(wù)處理以及向第三方系統(tǒng)發(fā)送數(shù)據(jù))
     * author: author
     * datetime: 2025/3/5上午11:44
     */
    public function import_sku($file)
    {
        // 定義處理每一行數(shù)據(jù)的回調(diào)函數(shù)
        $rowProcessor = function ($row) {
           // 這里可以根據(jù)實(shí)際需求處理每一行數(shù)據(jù)
            return [
                'column1' => $row[0],
                'column2' => $row[1],
                // 根據(jù)實(shí)際情況添加更多列
            ];
        };

        // 讀取數(shù)據(jù)(調(diào)用封裝的公共方法)
        $data = import_file_to_process_data($file, $rowProcessor);

        // 校驗(yàn)數(shù)據(jù)

        // 實(shí)例化 ProductSku 和 ProductSkuItem 模型
        $productSkuOrderModel = new ProductSku();
        $productSkuItemModel = new ProductSkuItem();

        // 開啟事務(wù)
        $productSkuOrderModel->startTrans();
        try {
            // 添加數(shù)據(jù)
            foreach ($data as $batch) {
                foreach ($batch as $item) {
                    $product_sku_id = $productSkuOrderModel->insertGetId($item);
                    $productSkuItemModel->create([
                        'column1'    => $item['column1'],
                        'column2'    => $item['column2'],
                        'column3'    => $item['column3'],
                       //...............................//
                    ]);
                }
            }
            // 提交事務(wù)
            $productSkuOrderModel->commit();
        } catch (\Exception $e) {
            // 回滾事務(wù)
            $productSkuOrderModel->rollback();
            throw_exception($e->getMessage());
        }
    }

四、常見問題與解決方法

(一)Generator 生成器數(shù)據(jù)使用問題

Generator 生成器的數(shù)據(jù)只能使用一次,如果需要多次使用,得重新調(diào)用相應(yīng)的方法。在本次導(dǎo)入功能中,如果不小心重復(fù)使用生成器數(shù)據(jù),可能會(huì)導(dǎo)致獲取不到數(shù)據(jù)的情況,需要特別注意。

(二)CSV 文件生成方式問題

不能直接將 Excel 文件復(fù)制一份并將后綴名改為 CSV,而是應(yīng)該打開 Excel 文件,點(diǎn)擊 “文件”,然后點(diǎn)擊 “另存為”,選擇 CSV 類型進(jìn)行保存,否則可能會(huì)出現(xiàn)獲取不到數(shù)據(jù)的情況。

五、總結(jié)

以上就是文件導(dǎo)入功能的詳細(xì)介紹,通過合理的函數(shù)封裝和邏輯處理,我們實(shí)現(xiàn)了一個(gè)兼容 Excel 和 CSV 文件的導(dǎo)入功能,同時(shí)保證了數(shù)據(jù)處理的效率和服務(wù)器空間的合理利用。在代碼實(shí)現(xiàn)過程中,我們充分考慮了錯(cuò)誤處理、事務(wù)管理以及大文件處理等場(chǎng)景,確保了功能的健壯性和可靠性。

六、未來演進(jìn)方向

異步處理架構(gòu)

fda8d23e23c226abc18476b08bf982d.png

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容