public function importExcel(): Json
{
// 设置脚本执行时间无限制,避免大数据处理超时
set_time_limit(0);
// 设置脚本最大内存使用量为2GB,处理大文件时避免内存溢出
ini_set('memory_limit', '2048M');
// 获取请求参数(包含上传的文件信息)
$params = $this->request->param();
// 验证文件是否上传
if (empty($params["file"])) {
return $this->fail('请上传要导入文件');
}
// 定义Excel表头与数据库字段的映射关系
$titleArr = [
'商家编号' => 'merchant_code',
'业务单号' => 'business_order_number',
'平台订单号' => 'platform_order_number',
'商家订单号' => 'merchant_order_number',
'退回件关联运单号' => 'return_waybill_number',
'店铺名称' => 'shop_name',
'下单时间' => 'order_time',
'始发省' => 'origin_province',
'始发市' => 'origin_city',
'目的省' => 'destination_province',
'目的市' => 'destination_city',
'计费重量' => 'billing_weight',
'实际重量(kg)' => 'actual_weight_kg',
'实际体积(cm3)' => 'actual_volume_cm3',
'首重单价' => 'first_weight_price',
'续重单价' => 'continued_weight_price',
'包裹/耗材数量' => 'package_material_quantity',
'销售平台订单号' => 'sales_platform_order_number',
'费用类型' => 'fee_type',
'原始金额' => 'original_amount',
'折扣/促销金额' => 'discount_promotion_amount',
'计费时间' => 'billing_time',
'结算金额' => 'settlement_amount',
'签收时间' => 'sign_time',
'商品编码' => 'product_code',
'商品名称' => 'product_name'
];
try {
// 构建上传文件的完整路径
$path = app()->getRootPath() . "public/" . $params['file'];
// 1. 创建Excel读取器并设置为只读取数据模式(优化内存)
$reader = IOFactory::createReaderForFile($path);
$reader->setReadDataOnly(true);
// 2. 获取工作表基本信息(总行数和最大列字母),避免加载整个文件
$worksheetInfo = $reader->listWorksheetInfo($path);
$totalRows = $worksheetInfo[0]['totalRows'];
$highestColumn = $worksheetInfo[0]['lastColumnLetter'];
// 3. 读取表头行(仅加载第一行数据)
$chunkFilter = new ChunkReadFilter();
$chunkFilter->setRows(1, 1);
$reader->setReadFilter($chunkFilter);
$spreadsheet = $reader->load($path);
$sheet = $spreadsheet->getSheet(0);
$title = $sheet->rangeToArray('A1:' . $highestColumn . '1', null, true, true, true)[1];
// 释放不再使用的Spreadsheet对象,减少内存占用
$spreadsheet->disconnectWorksheets();
unset($spreadsheet);
// 检查表头是否存在
if (empty($title)) {
throw new \Exception('Excel表头为空');
}
// 初始化数据库模型和计数器
$model = new DeliveryPrimaryRegionsItemizationModel();
//TODO:根据自身mysql所在服务的性能来控制批量插入大小 不然报错或者导入很慢 该值需要反复测验找到最合适的大小
$insertBatchSize = 2000; // 数据库插入批次大小
$total = 0; // 总记录数
$success = 0; // 成功处理记录数
$failedBatches = []; // 失败批次记录
$batchData = []; // 批次数据临时存储
$processedRows = 0; // 已处理行数
// 4. 分块处理Excel数据,每次处理$chunkSize行
//TODO:根据自身服务器性能来决定每次分块读取excel表格数据的条数 不然报错或者导入很慢 该值需要反复测验找到最合适的大小
$chunkSize = 5000; // 每次加载5000行
$chunkFilter = new ChunkReadFilter();
$reader->setReadFilter($chunkFilter);
// 循环处理每个数据块
for ($startRow = 2; $startRow <= $totalRows; $startRow += $chunkSize) {
$endRow = min($startRow + $chunkSize - 1, $totalRows);
$chunkFilter->setRows($startRow, $chunkSize);
// 5. 加载当前数据块
$spreadsheet = $reader->load($path);
$sheet = $spreadsheet->getSheet(0);
// 6. 处理当前块中的每一行数据
for ($row = $startRow; $row <= $endRow; $row++) {
// 获取当前行数据
$rowData = $sheet->rangeToArray('A' . $row . ':' . $highestColumn . $row, null, true, true, true)[$row];
$item = [];
// 将Excel列名映射为数据库字段名
foreach ($rowData as $colIndex => $value) {
$colName = $title[$colIndex] ?? '';
if ($colName && isset($titleArr[$colName])) {
$item[$titleArr[$colName]] = trim($value ?? ''); // 原始值去除首尾空格
}
}
// print_r($item);die;
// 如果行数据不为空,则添加到批次数据中
if (!empty($item)) {
$batchData[] = $item;
$processedRows++;
}
// 7. 当批次数据达到插入批次大小时,执行数据库插入操作
if (count($batchData) >= $insertBatchSize) {
$this->processBatch($model, $batchData, $success, $failedBatches, $total);
$batchData = []; // 清空批次数据
}
$total++; // 总记录数递增
}
// 8. 释放当前数据块占用的内存
$spreadsheet->disconnectWorksheets();
unset($spreadsheet);
gc_collect_cycles(); // 触发垃圾回收
// 9. 输出处理进度信息
$progress = round(($endRow / $totalRows) * 100, 2);
echo "处理进度: {$progress}% | 已处理: {$processedRows} 行\n";
}
// 10. 处理剩余不足一个批次的数据
if (!empty($batchData)) {
$this->processBatch($model, $batchData, $success, $failedBatches, $total);
}
// 返回处理结果
if (empty($failedBatches)) {
$resultMsg = "导入成功,共{$total}条数据,成功导入{$success}条";
return $this->success($resultMsg);
} else {
$errorCount = $total - $success;
$errorMsg = "失败 {$errorCount} 条,第一个错误: {$failedBatches[0]['error']}";
return $this->fail('导入失败', ['remark' => $errorMsg]);
}
} catch (\Exception $e) {
// 异常处理,返回错误信息
return $this->fail('导入过程中出错: ' . $e->getMessage());
}
}
/**
* 处理数据批次(优化SQL构建)
*/
private function processBatch($model, &$batchData, &$success, &$failedBatches, &$total)
{
// 如果批次数据为空则直接返回
if (empty($batchData)) return;
try {
// 使用参数绑定构建SQL语句,防止SQL注入
$tableName = $model->getTable();
$fields = array_keys($batchData[0]);
$fieldList = '`' . implode('`, `', $fields) . '`';
$placeholders = '(' . implode(',', array_fill(0, count($fields), '?')) . ')';
$sql = "INSERT INTO {$tableName} ({$fieldList}) VALUES ";
$values = [];
$params = [];
// 构建批量插入的SQL语句和参数数组
foreach ($batchData as $row) {
$values[] = $placeholders;
foreach ($fields as $field) {
$params[] = $row[$field] ?? null;
}
}
$sql .= implode(',', $values);
// 执行带参数绑定的SQL语句
// 一定不要使用模型去操作数据库 不然效率很低 原生的最好!
Db::execute($sql, $params);
// 更新成功记录数
$success += count($batchData);
// 清空批次数据
$batchData = [];
} catch (\Exception $e) {
// 记录失败批次信息
$failedBatches[] = [
'startRow' => $total - count($batchData) + 1,
'endRow' => $total,
'error' => $e->getMessage()
];
// 出错时清空批次数据,避免重复处理
$batchData = [];
}
}
/**
* 分块读取过滤器
* 用于控制只读取Excel文件中的指定行范围
* common.php代码不规范导致本地git有差异直接还原 直接写在控制器里算了!
*/
class ChunkReadFilter implements \PhpOffice\PhpSpreadsheet\Reader\IReadFilter
{
private $startRow = 0;
private $endRow = 0;
// 设置要读取的起始行和行数
public function setRows($startRow, $chunkSize) {
$this->startRow = $startRow;
$this->endRow = $startRow + $chunkSize;
}
// 判断当前单元格是否应该被读取
public function readCell($column, $row, $worksheetName = '') {
// 只读取指定范围内的行
if ($row >= $this->startRow && $row <= $this->endRow) {
return true;
}
return false;
}
}
导入非常的慢,慢的不行,另外我的excel表格当中的数据量为30万条,并且字段26个文字也特别多 ,怎么办呢?有什么可以优化的地方吗我的代码里面?如果可以优化提升性能请为我提供完整的代码,不要忘记注释信息
最新发布