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

ThinkPHP 6 + Uploadify + PHPExcel 實現 Excel 無刷新匯入完整實戰

     編輯:PHP 綜合知識

技術棧與版本需求

  • ThinkPHP 6.0+
  • jQuery 1.x (Uploadify 不相容 jQuery 2.x+)
  • Uploadify (Flash 上傳組件,現代專案建議改用 jQuery File Upload 或 Dropzone.js)
  • PHPExcel / 推薦 PhpSpreadsheet (PHPExcel 已停止維護)

前端頁面佈局

<!-- 引入必要資源 -->
<script src="/static/js/jquery-1.12.4.min.js"></script>
<script src="/static/uploadify/jquery.uploadify.min.js"></script>
<link rel="stylesheet" href="/static/uploadify/uploadify.css">

<!-- 上傳按鈕 -->
<input type="file" id="student_excel" name="student_excel" accept=".xls,.xlsx">

<!-- 進度模態框 (Amaze UI) -->
<div class="am-modal am-modal-loading am-modal-no-btn" tabindex="-1" id="upload-modal">
  <div class="am-modal-dialog">
    <div class="am-modal-hd">處理中</div>
    <div class="am-modal-bd" id="modal-content">正在上傳檔案...</div>
  </div>
</div>

JavaScript 上傳邏輯

$(function() {
    const uploadUrl = "";
    const importUrl = "";
    const sessionId = "";
    const $modal = $('#upload-modal');
    const $content = $('#modal-content');
    
    $('#student_excel').uploadify({
        'swf'      : '/static/uploadify/uploadify.swf',
        'uploader' : uploadUrl,
        'buttonText' : '選擇 Excel 檔案',
        'width'    : 150,
        'height'   : 36,
        'fileTypeExts' : '*.xls;*.xlsx',
        'formData' : { 'session_id': sessionId },
        'onUploadStart' : function(file) {
            $content.text('正在上傳檔案...');
            $modal.modal('open');
        },
        'onUploadSuccess' : function(file, data, response) {
            $content.text('正在解析匯入資料...');
            try {
                const result = JSON.parse(data);
                if (result.code !== 1) throw new Error(result.msg);
                
                // 發起匯入請求
                $.post(importUrl, { file: result.file }, function(res) {
                    $modal.modal('close');
                    if (res.code === 1) {
                        alert('匯入成功:' + res.msg);
                        location.reload();
                    } else {
                        alert('匯入失敗:' + res.msg);
                    }
                }, 'json');
            } catch (e) {
                $modal.modal('close');
                alert('伺服器回應異常:' + e.message);
            }
        },
        'onUploadError' : function(file, errorCode, errorMsg, errorString) {
            $modal.modal('close');
            alert('上傳失敗:' + errorString);
        }
    });
});

後端控制器實作

<?php
namespace app\admin\controller;

use think\Controller;
use think\facade\Filesystem;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;

class Student extends Controller
{
    // 上傳處理
    public function upload()
    {
        $file = request()->file('Filedata');
        if (empty($file)) {
            return json(['code' => 0, 'msg' => '未接收到檔案']);
        }
        
        // 驗證:副檔名、MIME、大小
        $allowedExt = ['xls', 'xlsx'];
        $ext = strtolower($file->getOriginalExtension());
        if (!in_array($ext, $allowedExt)) {
            return json(['code' => 0, 'msg' => '僅支援 .xls/.xlsx 格式']);
        }
        
        // 儲存到非公開目錄
        $savePath = 'uploads/excel/' . date('Ymd');
        $filename = $file->move(ROOT_PATH . 'public/' . $savePath);
        
        if (!$filename) {
            return json(['code' => 0, 'msg' => $file->getError()]);
        }
        
        return json([
            'code' => 1, 
            'file' => $savePath . '/' . $filename->getSaveName(),
            'msg' => '上傳成功'
        ]);
    }
    
    // 匯入處理
    public function import()
    {
        $relativePath = input('file');
        $fullPath = ROOT_PATH . 'public/' . $relativePath;
        
        if (!file_exists($fullPath)) {
            return json(['code' => 0, 'msg' => '檔案不存在']);
        }
        
        try {
            $spreadsheet = IOFactory::load($fullPath);
            $sheet = $spreadsheet->getActiveSheet();
            $rows = $sheet->toArray(null, true, true, true); // 索引為欄位字母
            
            $success = 0;
            $errors = [];
            
            // 跳過標題列 (假設第 1 列)
            foreach (array_slice($rows, 1) as $index => $row) {
                // 資料驗證
                if (empty($row['A']) || empty($row['B'])) {
                    $errors[] = "第 {$index} 行:學號或姓名為空";
                    continue;
                }
                
                $data = [
                    'student_no' => trim($row['A']),
                    'name'       => trim($row['B']),
                    'gender'     => (isset($row['C']) && $row['C'] === '男') ? 1 : 0,
                    'password'   => md5($row['D'] ?? '123456'), // 預設密碼
                    'create_time'=> time(),
                ];
                
                // 檢查重複
                if (\app\model\Student::where('student_no', $data['student_no'])->find()) {
                    $errors[] = "第 {$index} 行:學號 {$data['student_no']} 已存在";
                    continue;
                }
                
                \app\model\Student::create($data);
                $success++;
            }
            
            // 清理暫存檔
            @unlink($fullPath);
            
            return json([
                'code' => 1,
                'msg'  => "成功匯入 {$success} 筆" . ($errors ? ",失敗 " . count($errors) . " 筆" : "")
            ]);
            
        } catch (ReaderException $e) {
            return json(['code' => 0, 'msg' => 'Excel 解析失敗:' . $e->getMessage()]);
        } catch (\Exception $e) {
            return json(['code' => 0, 'msg' => '系統錯誤:' . $e->getMessage()]);
        }
    }
}

PhpSpreadsheet 關鍵用法補充

// 讀取大檔案優化:使用 ReadFilter 僅讀取必要範圍
class ChunkReadFilter implements \PhpOffice\PhpSpreadsheet\Reader\IReadFilter
{
    private $startRow = 0;
    private $endRow = 0;
    
    public function setRows($start, $end) {
        $this->startRow = $start;
        $this->endRow = $end;
    }
    
    public function readCell($column, $row, $worksheetName = '') {
        return ($row >= $this->startRow && $row < $this->endRow);
    }
}

// 分塊讀取大檔 (每次 1000 列)
$filter = new ChunkReadFilter();
$reader = IOFactory::createReader('Xlsx');
$reader->setReadFilter($filter);

$chunkSize = 1000;
for ($start = 2; $start <= $totalRows; $start += $chunkSize) {
    $filter->setRows($start, min($start + $chunkSize, $totalRows + 1));
    $spreadsheet = $reader->load($fullPath);
    // 處理 $spreadsheet->getActiveSheet()->toArray()
    unset($spreadsheet); // 釋放記憶體
}

安全性與健壯性強化

  1. 檔案驗證:檢查 MIME 型別 ($file->getMimeType())、魔術位元組、大小限制
  2. 儲存隔離:上傳目錄設為不可執行 (Nginx location ~ \.php$ { deny all; })
  3. 資料清洗:trim()、htmlspecialchars()、長度限制、格式驗證
  4. 事務處理:匯入過程包裹在 Db::transaction() 中,失敗回滾
  5. 併發控制:使用 Redis 分散式鎖防止重複匯入
  6. 日誌審計:記錄匯入操作者、時間、筆數、錯誤詳情

現代化替代方案建議

Uploadify 基於 Flash,已被瀏覽器淘汰。建議遷移至:

  • Dropzone.js:純 HTML5、拖拽上傳、進度條、圖片預覽
  • jQuery File Upload:功能最完整、支援分塊上傳、斷點續傳
  • Uppy:模組化、支援多來源 (Google Drive, Instagram 等)、React/Vue 整合佳

總結

本文展示了 ThinkPHP 6 整合前端上傳組件與 PhpSpreadsheet 實現 Excel 無刷新匯入的完整流程。關鍵點在於前後端分離的非同步協作:上傳 → 回傳檔案路徑 → 後端異步解析入庫 → 前端輪詢/回調顯示結果。實際專案中務必加強檔案驗證、資料清洗、異常處理與記憶體優化。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved