一、引言
在上篇文章中,我們探討了文件導(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)思路
(一)整體流程
- 前端上傳文件 :前端負(fù)責(zé)將需要導(dǎo)入的文件(支持 Excel 和 CSV 格式)上傳至后端。
- 后端接收并上傳到服務(wù)器 :后端接收到文件后,將其上傳到服務(wù)器進(jìn)行臨時(shí)存儲(chǔ)。
- 讀取文件內(nèi)容 :根據(jù)文件類型(Excel 或 CSV),調(diào)用相應(yīng)的函數(shù)讀取文件內(nèi)容,獲取數(shù)據(jù)。
- 存儲(chǔ)到數(shù)據(jù)庫 :將讀取到的數(shù)據(jù)進(jìn)行處理后存儲(chǔ)到數(shù)據(jù)庫中。
- 刪除服務(wù)器文件 :為避免服務(wù)器空間被占用,文件讀取和處理完成后,及時(shí)刪除服務(wù)器上的臨時(shí)文件。
- 給前端提示 :向前端返回導(dǎo)入結(jié)果,提示導(dǎo)入是否成功。
以下是整體流程圖:

(二)技術(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)
