首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHPExcel 匯入 Excel 資料完整實作:驗證、錯誤報表與批次入庫

     編輯:PHP 相關知識

本文展示一個生產級的 Excel 匯入流程:上傳檔案 → 解析表頭 → 資料驗證 (必填、重複、格式) → 錯誤報表輸出 (Excel) → 批次寫入資料庫。採用 PHPExcel (現建議升級至 PhpSpreadsheet) 並示範分批處理大量資料以避免記憶體耗盡。

完整流程架構

<?php
public function importProductBasicInfo($data) {
    // 1. 載入核心類別 (建議改用 PhpSpreadsheet)
    require_once 'PHPExcel.php';
    require_once 'PHPExcel/IOFactory.php';
    require_once 'PHPExcel/Reader/Excel5.php';

    $error = [];           // 錯誤收集器
    $resultInfo = null;    // 統一回傳結構
    $needNext = true;

    // 2. 檔案上傳處理
    $fileName = $_FILES['productinfo']['name'];
    $filePath = CBase_Common_UploadPicture::uploadFile($data['productinfo'], 'product');

    // 上傳錯誤代碼對應
    if ($filePath === 1) { $error[1] = '檔案超過 upload_max_filesize 限制'; }
    elseif ($filePath === 4) { $error[4] = '無檔案上傳'; }
    else {
        // 3. 讀取 Excel (僅讀資料模式節省記憶體)
        $objReader = PHPExcel_IOFactory::createReader('Excel5');
        $objReader->setReadDataOnly(true);
        $objPHPExcel = $objReader->load($filePath);
        $objWorksheet = $objPHPExcel->getActiveSheet();

        $highestRow = $objWorksheet->getHighestRow();
        $highestColumn = $objWorksheet->getHighestColumn();
        $highestColumnIndex = PHPExcel_Cell::columnIndexFromString($highestColumn);

        // 4. 解析表頭對應欄位
        $colums = [];
        for ($i = 0; $i < $highestColumnIndex; $i++) {
            $cValue = trim($objWorksheet->getCellByColumnAndRow($i, 1)->getValue());
            // 依專案常數對應欄位鍵名
            $map = [
                self::PRODUCT_SAP_CODE     => 'sap_code',
                self::PRODUCT_NAME         => 'pname',
                self::PRODUCT_GROUP        => 'product_group',
                self::PRODUCT_BRAND        => 'product_brand',
                self::PRODUCT_PROXY_FLAG   => 'product_proxy_flag',
                self::PRODUCT_BINNING      => 'product_binning',
                self::PRODUCT_SELL_PICK    => 'product_sell_pick',
                self::PRODUCT_ATTRIBUTE    => 'product_attribute',
                self::PRODUCT_SUPPLIER_CODE=> 'vendor_code',
                self::PRODUCT_SUPPLY_ADDRESS=> 'zzwerk_code',
                self::PRODUCT_BATCH        => 'zzlgort_code',
            ];
            $colums[$i] = $map[$cValue] ?? null;
            if (!$colums[$i]) $error[3][] = $cValue; // 未識別表頭
        }

        // 5. 基礎驗證
        $dataCount = $highestRow - 1;
        if (empty($colums)) $error[5] = '缺少表頭';
        elseif (!in_array('sap_code', $colums)) $error[2] = '表頭缺少 SAP 編碼';
        elseif ($dataCount <= 0) $error[6] = 'Excel 無資料列';

        // 6. 逐列資料驗證與收集
        if (empty($error)) {
            $execlAllShopLinkedId = [];
            $excelIdRow = [];
            $allProduct = [];

            // 第一輪:掃描重複、收集 SAP 代碼
            for ($i = 2; $i <= $highestRow; $i++) {
                $colKey = array_search('sap_code', $colums);
                $sap = trim($objWorksheet->getCellByColumnAndRow($colKey, $i)->getValue());
                if (!$sap) continue;

                if (in_array($sap, $execlAllShopLinkedId)) {
                    $error[7][$sap]['duplicate'] = true;
                    $error[7][$sap]['excelRow'][] = $i;
                } else {
                    $excelIdRow[$sap] = $i;
                    $execlAllShopLinkedId[$i] = $sap;
                }
            }

            // 分批查詢資料庫既有資料 (每 1000 筆)
            $dealMultiple = ceil($dataCount / 1000);
            for ($batch = 0; $batch < $dealMultiple; $batch++) {
                $offset = $batch * 1000 + 2;
                $max = min(($batch + 1) * 1000 + 1, $highestRow);
                $batchIds = [];
                for ($j = $offset; $j <= $max; $j++) {
                    if (!empty($execlAllShopLinkedId[$j])) $batchIds[] = $execlAllShopLinkedId[$j];
                }
                $dbShopProducts = $this->getShopLinkedByIds($batchIds); // 需自行實作

                // 第二輪:逐欄驗證必填、格式
                for ($j = $offset; $j <= $max; $j++) {
                    $product = [];
                    for ($k = 0; $k < $highestColumnIndex; $k++) {
                        $val = trim($objWorksheet->getCellByColumnAndRow($k, $j)->getValue());
                        if ($val !== '' && isset($colums[$k])) $product[$colums[$k]] = $val;
                    }
                    $id = $product['sap_code'] ?? '';
                    if (!$id) continue;

                    // 檢查資料庫是否已存在
                    if (!in_array($id, $dbShopProducts)) {
                        $allProduct[$id] = $product;
                    } else {
                        $error[7][$id]['hasId'] = true;
                    }

                    // 必填欄位檢查
                    $required = ['pname','product_group','product_brand','product_proxy_flag',
                                 'product_binning','product_sell_pick','product_attribute',
                                 'vendor_code','zzwerk_code','zzlgort_code'];
                    foreach ($required as $field) {
                        if (!isset($product[$field])) $error[7][$id]['empty'.$field] = true;
                    }
                    if (isset($error[7][$id])) $error[7][$id]['excelRow'] = $j;
                }
            }
        }

        // 7. 結果處理:有錯誤 → 產出錯誤報表 Excel;無錯誤 → 批次入庫
        $resultInfo['fileName'] = $fileName;
        if (!empty($error)) {
            // 依優先順序決定錯誤類型
            $typeMap = [1=>1, 2=>2, 3=>3, 4=>4, 6=>6, 7=>7];
            foreach ($typeMap as $code => $type) {
                if (isset($error[$code])) {
                    $resultInfo['type'] = $type;
                    $resultInfo['msg'] = $code === 3
                        ? '表頭【'.implode(',',$error[3]).'】不存在'
                        : ($error[$code] ?? '檔案資料有誤');
                    break;
                }
            }

            // 產出錯誤報表 (type 7)
            if ($resultInfo['type'] === 7) {
                $objWrite = new PHPExcel();
                $objWrite->getProperties()->setCreator('System')->setTitle('ErrorReport');
                $sheet = $objWrite->getActiveSheet();
                $sheet->setTitle('錯誤報告');
                // 寫入表頭...
                $headers = ['SAP編碼','原Excel行號','重複行號','商品名','商品群組','品牌','代銷旗標',
                            '裝箱清單','先銷後採','屬性','供應商代碼','供應地點','庫區','其他'];
                foreach ($headers as $idx => $h) {
                    $sheet->setCellValueByColumnAndRow($idx, 1, $h);
                    $sheet->getColumnDimensionByColumn($idx)->setWidth(20);
                }
                $row = 2;
                foreach ($error[7] as $pid => $info) {
                    $sheet->setCellValueByColumnAndRow(0, $row, isset($info['hasId']) ? "$pid-已存在" : $pid);
                    $sheet->setCellValueByColumnAndRow(1, $row, $info['excelRow']);
                    if (isset($info['duplicate'])) $sheet->setCellValueByColumnAndRow(2, $row, $excelIdRow[$pid]);
                    foreach (['emptyName'=>3,'emptyProductGroup'=>4,'emptyProductBrand'=>5,
                              'emptyProductProxyFlag'=>6,'emptyProductBinning'=>7,
                              'emptyProductSellPick'=>8,'emptyProductAttribute'=>9,
                              'emptyVendorCode'=>10,'emptyZzwerkCode'=>11,'emptyZzlgortCode'=>12] as $key => $col) {
                        if (isset($info[$key])) $sheet->setCellValueByColumnAndRow($col, $row, '-為空');
                    }
                    $row++;
                }
                $writer = PHPExcel_IOFactory::createWriter($objWrite, 'Excel5');
                $reportName = date('Y_m_d_H_i_s').'_'.mt_rand(1,99).'_'.pathinfo($fileName, PATHINFO_FILENAME).'ErrorReport.xls';
                $reportPath = FILE_PATH . DS . 'feedback' . DS . $reportName;
                $writer->save($reportPath);
                $resultInfo['errorReport'] = $reportName;
            }
        } else {
            // 8. 批次入庫 (示範 SQL 組裝,實際建議使用 Prepared Statement)
            foreach ($allProduct as $id => $p) {
                $sql = "INSERT INTO yr_product SET ";
                $fields = [];
                // SAP 編碼左補 0 至 18 碼
                if (isset($p['sap_code']) && strlen($p['sap_code']) < 18) {
                    $p['sap_code'] = str_pad($p['sap_code'], 18, '0', STR_PAD_LEFT);
                }
                foreach (['pname','sap_code','product_group','product_brand','product_proxy_flag',
                          'product_binning','product_sell_pick','product_attribute',
                          'vendor_code','zzwerk_code','zzlgort_code'] as $f) {
                    if (isset($p[$f])) $fields[] = "`$f` = '" . addslashes($p[$f]) . "'";
                }
                $sql .= implode(', ', $fields);
                $this->excuteMultiInsertSql($sql); // 需自行實作
            }
            $resultInfo['type'] = 8;
            $resultInfo['msg'] = '匯入商品基本資料成功';
        }
        return $resultInfo;
    }
}
?>

關鍵設計重點

  • 分批處理:每 1000 列查詢一次資料庫,避免 IN (...) 過長或記憶體暴增。
  • 錯誤分級:檔案層級錯誤 (上傳、表頭) 直接中止;資料列錯誤收集後統一匯出報表。
  • 報表自動產生:將驗證失敗明細寫入新 Excel,方便業務人員修正再匯入。
  • SQL 注入防護:範例使用 addslashes,實務上務必改用 PDO Prepared Statements。

升級建議

  1. 將 PHPExcel 替換為 PhpSpreadsheet (已停止維護),命名空間對應調整即可。
  2. 引入 Laravel Excel 或 Box/Spout 以流式讀寫大型檔案 (GB 級)。
  3. 將驗證規則抽離為獨立 Validator 類別,支援單元測試。
  4. 入庫改用 INSERT ... ON DUPLICATE KEY UPDATE 或批次 INSERT 提升效能。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved