ThinkPHP导入Excel需严格校验:上传前验证文件大小、扩展名及真实MIME类型;读取后校验表头一致性与必填项;逐行校验空值、格式及业务规则;精准识别并转换日期、数字、文本型数字;最后通过事务控制与内存管理完成批量入库。
在ThinkPHP中导入Excel时,必须对读取到的每一行数据进行结构校验、业务规则判断与类型清洗,否则脏数据会直接入库导致业务逻辑崩溃或报表失真——比如手机号字段混入空格和中文括号、日期被识别为科学计数、数字型ID变成浮点导致主键冲突。
调用 request()->file('excel') 获取文件对象后,立即执行双重验证:
第一步:用 validate(['size' => 1024 * 1024 * 5, 'ext' => ['xlsx', 'xls']]) 限制最大5MB且仅允许两种扩展名;【注意:仅校验后缀不安全,必须配合 MIME 真实类型检查】
第二步:启用 PHP fileinfo 扩展,调用 $file->getOriginalMime() 比对是否为 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet(.xlsx)或 application/vnd.ms-excel(.xls),不匹配则直接拒绝。
立即学习“PHP免费学习笔记(深入)”;
未启用 fileinfo 扩展会导致 Unable to determine mime type 错误,且无法拦截伪造后缀的恶意文件。
成功加载 Excel 后,先提取第一行作为表头数组,再与预设的合法字段白名单比对:
方法一:硬编码比对(适用于固定模板)
$header = $worksheet->getRowIterator()->current()->getCellIterator()->toArray();
$expected = ['姓名', '手机号', '入职日期', '部门'];
if (array_values($header) !== $expected) { throw new Exception('表头格式错误:期望[' . implode('、', $expected) . '],实际为[' . implode('、', array_values($header)) . ']'); }
方法二:模糊匹配(兼容空格、全角/半角、顺序错位)
遍历实际表头,用 mb_strtolower(str_replace([' ', ' ', '(', ')'], '', $cell->getValue())) 清洗后与小写关键词匹配,例如“mobile”“tel”“电话”都映射到“手机号”字段;【关键:必须跳过合并单元格残留的 NULL 或空字符串,否则 array_values() 会错位】
使用 getRowIterator() 遍历时,对每行数据执行三层过滤:
① 全空行跳过:empty(array_filter($values, function($v) { return is_string($v) ? trim($v) !== '' : !is_null($v) && $v !== ''; }))
② 字段级非空校验:对“姓名”“手机号”等必填字段,检查 trim($row['姓名']) === '',报错并记录行号;手机号需用 preg_match('/^1[3-9]d{9}$/', str_replace(['-', ' ', '(', ')'], '', $row['手机号'])) 剔除干扰字符后再验证。
③ 业务唯一性校验:在入库前查库确认“手机号”未重复,避免事务中抛出唯一索引异常导致整批回滚——这一步必须放在单行校验末尾,不能等到全部读完再批量查。
Excel 单元格值默认是原始类型,需主动识别并强转:
日期字段:先判断 $cell->getDataType() === PhpOfficePhpSpreadsheetCellDataType::TYPE_NUMERIC,再结合样式格式码匹配 yyyy-mm-dd 或 yyyy/m/d,最后用 PhpOfficePhpSpreadsheetSharedDate::excelToDateTimeObject($cell->getValue()) 转为 DateTime 对象;【若跳过样式判断直接转换,2025年3月15日会被当成数字45721写入库】
数字字段:对“工资”“数量”等字段,用 filter_var($value, FILTER_VALIDATE_FLOAT) 校验是否为有效数字,无效则标记为错误行;
文本型数字:如“工号”字段含前导零(00123),Excel 默认读成整数 123,必须用 $cell->getStyle()->getNumberFormat()->getFormatCode() 判断是否为文本格式(如 '@'),是则强制 (string)$cell->getValue() 保留原始字符串。
将所有通过校验的行暂存为二维数组后,执行最后一道防线:
第一步:用 Db::transaction(function () use ($validData) { ... }); 包裹插入逻辑,确保整批失败可回滚;
第二步:对每条数据调用模型的 validate(true) 方法,触发 ThinkPHP 内置验证规则(如 email 格式、length 最大长度);
第三步:设置 ini_set('memory_limit', '512M') 并在每插入 1000 行后调用 gc_collect_cycles(),防止内存持续增长触发超限;
第四步:捕获 PhpOfficePhpSpreadsheetException 和 thinkdbexceptionDataNotFoundException 等特定异常,将错误信息连同行号写入日志,返回前端可读提示(如“第47行:手机号格式错误”)。