必须使用InnoDB引擎并开启事务,通过Filesystem安全保存临时文件,手动判断扩展名加载对应Reader并启用只读模式,分批插入且禁止闭包内try-catch,校验失败时抛出带行号的异常。

ThinkPHP导入Excel时,一旦数据写入中途出错,必须让已入库的记录全部撤回,否则会出现「部分成功」的脏数据状态。
确认数据库表引擎支持事务
执行 SHOW CREATE TABLE `user`; 查看建表语句末尾是否含 ENGINE=InnoDB。MyISAM 表执行 Db::rollback() 会静默成功但实际无效,所有回滚测试都将在该前提下彻底失效。
若显示 ENGINE=MyISAM,需先执行 ALTER TABLE `user` ENGINE=InnoDB; 转换引擎,否则后续所有事务控制均无意义。
上传后安全保存临时文件
不要用 copy() 或 move_uploaded_file() 直接操作 $_FILES['file']['tmp_name']——路径拼错、Windows/Linux 斜杠不一致、运行用户无写权限都会导致文件丢失或覆盖。
立即学习“PHP免费学习笔记(深入)”;
使用 ThinkPHP 内置 Filesystem 组件存文件:$path = \think\facade\Filesystem::putFile('upload', $file);,它自动处理目录创建、唯一文件名生成与权限适配。
这一步失败时,【$path 返回 false】,应立即中止流程,不进入后续解析环节。
按格式选择对应 Reader 并启用只读模式
方法一:用 IOFactory 自动探测(简单但不可靠)
不推荐用于生产环境,.xlsx 文件可能被误判为 .csv 导致解析崩溃。
解析 MySQL 8.0 的 DDL 脚本,并将表结构导出为 Excel 格式。适用于用户需将 MySQL 的 `CREATE TABLE` 语句转换为结构化的 Excel 文件场景,导出内容包含字段名、数据类型、长度及精度等信息。支持多张表的批量处理,每张表对应 Excel 中的一个独立工作表。适用于数据库文档编写、数据字典生成及模式分析等任务。
方法二:手动判断扩展名后实例化指定 Reader(推荐)
① 获取原始文件名:$ext = pathinfo($file->getOriginalName(), PATHINFO_EXTENSION);
② 根据扩展名加载对应 Reader:$reader = strtolower($ext) === 'xlsx' ? new \PhpOffice\PhpSpreadsheet\Reader\Xlsx() : new \PhpOffice\PhpSpreadsheet\Reader\Xls();
③ 必须调用 $reader->setReadDataOnly(true); ——跳过样式、公式、合并单元格解析,避免内存溢出与日期格式错乱。
开启事务并分批写入数据
使用 Db::transaction() 闭包方式包裹整个导入逻辑,它会在闭包内抛出未捕获异常时自动触发 rollback:
Db::transaction(function () use ($spreadsheet, $mappedFields) {<br> $sheet = $spreadsheet->getActiveSheet();<br> $rows = [];<br> for ($i = 2; $i <= $sheet->getHighestRow(); $i++) {<br> $row = [];<br> foreach ($mappedFields as $col => $field) {<br> $row[$field] = $sheet->getCell($col . $i)->getValue();<br> }<br> $rows[] = $row;<br> if (count($rows) >= 500) {<br> Db::name('user')->insertAll($rows);<br> $rows = [];<br> }<br> }<br> if (!empty($rows)) {<br> Db::name('user')->insertAll($rows);<br> }<br>});
注意:闭包内不能出现任何 try-catch,否则异常被捕获后不会穿透,框架无法感知错误,【事务将静默提交】。
解析失败时精准定位错误行
在循环读取每一行前加校验逻辑,例如检查手机号格式、邮箱合法性、必填字段是否为空:
if (empty($row['mobile']) || !preg_match('/^1[3-9]\d{9}$/', $row['mobile'])) {<br> throw new \Exception("第 {$i} 行手机号格式错误");<br>}
这样异常消息自带行号,前端可直接展示具体哪一行出错,无需人工逐行排查。


















