应使用结构化数据库表(如import_logs)替代文本日志,字段需含import_id、status、时间戳、计数器及TEXT型log_message,并实时分批更新、保留上下文、加索引与清理策略。

导入前先创建结构化日志记录表
直接往文本文件里追加日志容易丢行、乱序、难排查,尤其并发导入或大文件分批处理时。建议用数据库表存日志,字段至少包含:import_id(唯一批次号)、file_name、status(pending/running/success/failed)、start_time、end_time、processed_count、error_count、log_message(TEXT 类型,存关键错误或跳过行信息)。
示例建表语句:
CREATE TABLE import_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
import_id VARCHAR(32) NOT NULL,
file_name VARCHAR(255),
status ENUM('pending','running','success','failed') DEFAULT 'pending',
start_time DATETIME,
end_time DATETIME,
processed_count INT DEFAULT 0,
error_count INT DEFAULT 0,
log_message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-
import_id建议用uniqid('imp_')或md5($_FILES['file']['name'] . time())生成,确保同文件多次导入可区分 - 不要把整条学生数据 JSON 化塞进
log_message,只记关键上下文,比如“第127行:手机号格式错误(138xxxx999)” - 避免在循环内频繁 INSERT,可先收集错误行,最后批量写入一条日志记录
Excel 解析时实时写入中间态日志
用 PhpSpreadsheet 读取时,别等全量解析完再记日志——万一卡在第 5000 行,前面的进度就丢了。应在每处理 N 行(如 100 行)或检测到异常时,更新一次数据库中的 processed_count 和 log_message。
关键代码片段:
立即学习“PHP免费学习笔记(深入)”;
$importId = uniqid('imp_');
$pdo->prepare("INSERT INTO import_logs (import_id, file_name, status, start_time) VALUES (?, ?, 'running', NOW())")->execute([$importId, $fileName]);
// 处理循环中
foreach ($rows as $index => $row) {
if ($index % 100 === 0 && $index > 0) {
$pdo->prepare("UPDATE import_logs SET processed_count = ?, log_message = CONCAT(log_message, ?) WHERE import_id = ?")
->execute([$index, "\n[INFO] 已处理 {$index} 行", $importId]);
}
// ……校验、入库逻辑
if ($validationFailed) {
$errorLog = "[ERROR] 第{$index}行:{$errorMessage}";
$pdo->prepare("UPDATE import_logs SET error_count = error_count + 1, log_message = CONCAT(log_message, ?) WHERE import_id = ?")
->execute([$errorLog, $importId]);
}
}
- 用
CONCAT(log_message, ?)而非覆盖,保留时间线顺序 - 避免在事务内做大量日志更新,否则拖慢主流程;可将日志写入设为非事务性(如用另一 PDO 连接)
- 若用
fputcsv写文件日志,务必加fflush(),否则缓冲区未刷盘就中断,日志就断在半截
导入失败后能准确定位并重试
用户传了个 2000 行 Excel,报错“第 883 行导入失败”,但没留原始行内容和错误堆栈,根本没法复现。必须让日志带上下文。
- 捕获
try/catch中的Exception或PDOException,用$e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine()记入log_message - 对每一行学生数据,生成临时数组包含原始单元格值(如
['name' => '张三', 'phone' => '138xxx']),出错时json_encode($rawRow)一并记下 - 提供「按 import_id 查看完整日志」接口,前端渲染成折叠式列表,支持复制某行原始数据用于调试
- 不建议自动重试——网络超时可重试,但“身份证重复”这种业务错误重试只会再错一遍
日志权限与清理策略不能忽略
日志表会越积越大,尤其测试环境反复上传。线上跑一个月,import_logs 可能达百万行,查起来卡,备份也慢。
- 给日志表加联合索引:
KEY idx_import_status_time (import_id, status, created_at),方便按批次+状态快速检索 - 设置定时任务(如每天凌晨)清理 90 天前的
status = 'success'记录,保留failed至少 180 天 -
log_message字段别设太小(至少 TEXT),但也要防恶意超长输入——入库前用mb_strimwidth($msg, 0, 2000, '...')截断 - 日志内容可能含手机号、姓名等敏感字段,导出或查看接口必须走登录鉴权,且禁止前端直接
echo $log_message,要 HTML 转义
真正难的不是记日志,是让日志在出问题时能一句话告诉你“谁、什么时候、对哪个文件、卡在哪一行、为什么卡”。字段设计、更新时机、上下文保全,这三处漏掉任何一环,日志就只剩占磁盘空间的份儿了。











