diff --git a/application/admin/library/traits/Backend.php b/application/admin/library/traits/Backend.php index fc7032b509ab0e943097ca99ed700c9d66d5561a..d0aa3f9ac0b3f9cb2cb2b67230cdf57e9e3a3a89 100755 --- a/application/admin/library/traits/Backend.php +++ b/application/admin/library/traits/Backend.php @@ -4,11 +4,12 @@ namespace app\admin\library\traits; use app\admin\library\Auth; use Exception; +use PhpOffice\PhpSpreadsheet\IOFactory; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Reader\IReadFilter; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; -use PhpOffice\PhpSpreadsheet\Reader\Xlsx; -use PhpOffice\PhpSpreadsheet\Reader\Xls; -use PhpOffice\PhpSpreadsheet\Reader\Csv; use think\Db; +use think\Hook; use think\db\exception\BindParamException; use think\db\exception\DataNotFoundException; use think\db\exception\ModelNotFoundException; @@ -350,132 +351,283 @@ trait Backend */ protected function import() { + + // 后台运行 + ignore_user_abort(true); + + // 取消超时 + set_time_limit(0); + + $timer1 = microtime(true); + $usage1 = memory_get_usage(); + + //取得上传文件 + $file = $this->request->request('file'); if (!$file) { $this->error(__('Parameter %s can not be empty', 'file')); } - $filePath = ROOT_PATH . DS . 'public' . DS . $file; + + $filePath = ROOT_PATH.DS.'public'.DS.$file; if (!is_file($filePath)) { $this->error(__('No results were found')); } - //实例化reader - $ext = pathinfo($filePath, PATHINFO_EXTENSION); - if (!in_array($ext, ['csv', 'xls', 'xlsx'])) { - $this->error(__('Unknown data format')); - } - if ($ext === 'csv') { - $file = fopen($filePath, 'r'); - $filePath = tempnam(sys_get_temp_dir(), 'import_csv'); - $fp = fopen($filePath, 'w'); - $n = 0; - while ($line = fgets($file)) { - $line = rtrim($line, "\n\r\0"); - $encoding = mb_detect_encoding($line, ['utf-8', 'gbk', 'latin1', 'big5']); - if ($encoding !== 'utf-8') { - $line = mb_convert_encoding($line, 'utf-8', $encoding); - } - if ($n == 0 || preg_match('/^".*"$/', $line)) { - fwrite($fp, $line . "\n"); - } else { - fwrite($fp, '"' . str_replace(['"', ','], ['""', '","'], $line) . "\"\n"); - } - $n++; - } - fclose($file) || fclose($fp); - $reader = new Csv(); - } elseif ($ext === 'xls') { - $reader = new Xls(); - } else { - $reader = new Xlsx(); + $insert = []; + $encode = 'utf-8'; + $charSet = setlocale(LC_CTYPE, 0); + $fileType = strtolower(IOFactory::identify($filePath)); + + // 检查文件类型 + + if (!in_array($fileType, ['csv', 'xls', 'xlsx'])) { + $this->error(__('Unknown data format')); } //导入文件首行类型,默认是注释,如果需要使用字段名称请使用name $importHeadType = isset($this->importHeadType) ? $this->importHeadType : 'comment'; - $table = $this->model->getQuery()->getTable(); + $model = get_class($this->model); + $table = $this->model->getQuery()->getTable(); $database = \think\Config::get('database.database'); $fieldArr = []; - $list = db()->query("SELECT COLUMN_NAME,COLUMN_COMMENT FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND TABLE_SCHEMA = ?", [$table, $database]); + $list = db()->query("SELECT COLUMN_NAME,COLUMN_COMMENT FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND TABLE_SCHEMA = ?", [$table, $database]); foreach ($list as $k => $v) { - if ($importHeadType == 'comment') { - $v['COLUMN_COMMENT'] = explode(':', $v['COLUMN_COMMENT'])[0]; //字段备注有:时截取 + if ('comment' == $importHeadType) { + $v['COLUMN_COMMENT'] = explode(':', $v['COLUMN_COMMENT'])[0]; //字段备注有:时截取 $fieldArr[$v['COLUMN_COMMENT']] = $v['COLUMN_NAME']; } else { $fieldArr[$v['COLUMN_NAME']] = $v['COLUMN_NAME']; } } + // 取得csv文件编码 + + if ('csv' === $fileType) { + + // 读取文件的前 1KB 进行检测 + + $handle = fopen($filePath, 'r'); + + rewind($handle); + $snippet = fread($handle, 1024); + fclose($handle); + + $encodes = ['utf-8', 'gbk', 'gb18030', 'latin1', 'big5']; + $detected = mb_detect_encoding($snippet, $encodes); + + if (false !== $detected) { + $encode = $detected; + } + } + //加载文件 - $insert = []; + try { - if (!$PHPExcel = $reader->load($filePath)) { - $this->error(__('Unknown data format')); + + // windows系统php7环境csv编码解析问题 + + if ('C' !== $charSet) { + setlocale(LC_CTYPE, 'C'); } - $currentSheet = $PHPExcel->getSheet(0); //读取文件中的第一个工作表 - $allColumn = $currentSheet->getHighestDataColumn(); //取得最大的列号 - $allRow = $currentSheet->getHighestRow(); //取得一共有多少行 - $maxColumnNumber = Coordinate::columnIndexFromString($allColumn); - $fields = []; - for ($currentRow = 1; $currentRow <= 1; $currentRow++) { - for ($currentColumn = 1; $currentColumn <= $maxColumnNumber; $currentColumn++) { - $val = $currentSheet->getCellByColumnAndRow($currentColumn, $currentRow)->getValue(); - $fields[] = $val; - } + + // 加载表格 + + $fileReader = IOFactory::createReaderForFile($filePath); + $fileReader->setReadDataOnly(true); + $sheetlist = $fileReader->listWorksheetInfo($filePath); + $sheetInfo = isset($sheetlist[0]) ? (object) $sheetlist[0] : null; + + if (empty($sheetInfo)) { + throw new Exception('读取工作表信息失败'); + } + + if ('csv' === $fileType) { + $fileReader->setInputEncoding($encode); } - for ($currentRow = 2; $currentRow <= $allRow; $currentRow++) { - $values = []; - for ($currentColumn = 1; $currentColumn <= $maxColumnNumber; $currentColumn++) { - $val = $currentSheet->getCellByColumnAndRow($currentColumn, $currentRow)->getValue(); - $values[] = is_null($val) ? '' : $val; + // 分块读取配置 + + $header = []; + $chunkSize = 10000; + $chunkSheet = new Spreadsheet(); + $chunkFilter = new IReadFilter\ChunkReadFilter(); + $chunkReader = IOFactory::createReaderForFile($filePath); + $chunkReader->setReadDataOnly(true); + $chunkReader->setReadFilter($chunkFilter); + + if ('csv' === $fileType) { + $chunkReader->setContiguous(true); + $chunkReader->setInputEncoding($encode); + } + + // 分块读取数据 + + for ($currentRow = 1; $currentRow <= $sheetInfo->totalRows; $currentRow += $chunkSize) { + + $chunkFilter->setChunk($currentRow, $chunkSize); + $chunkReader->setLoadSheetsOnly($sheetInfo->worksheetName); + + if ('csv' === $fileType) { + $chunkReader->setSheetIndex(0); + $chunkSheet = $chunkReader->loadIntoExisting($filePath, $chunkSheet); + } else { + $chunkSheet = $chunkReader->load($filePath); } - $row = []; - $temp = array_combine($fields, $values); - foreach ($temp as $k => $v) { - if (isset($fieldArr[$k]) && $k !== '') { - $row[$fieldArr[$k]] = $v; + + $activeSheet = $chunkSheet->getSheet(0); + + // 取得当前工作表名称 + $worksheetName = $activeSheet->getTitle(); + // 取得最大的列号(ABC..) + $lastColumnLetter = $activeSheet->getHighestDataColumn(); + // 取得最大的列索引(123..) + $lastColumnIndex = Coordinate::columnIndexFromString($lastColumnLetter); + // 取得总行数 + $totalRows = $activeSheet->getHighestRow(); + // 取得总列数 + $totalColumns = $lastColumnIndex + 1; + + // 转为数组 + $sheetRange = sprintf('A1:%s%d', $lastColumnLetter, $totalRows); + $sheetArray = $activeSheet->rangeToArray($sheetRange, null, false, false, false); + + // 数据处理 + if (!empty($sheetArray)) { + + $startIndex = 1; + $currentIndex = 1; + + if (1 == $currentRow) { + $startIndex++; + $header = $sheetArray[0]; + } + + foreach ($sheetArray as $row) { + if ($currentIndex >= $startIndex) { + foreach ($row as &$cell) { + $cell = is_null($cell) ? '' : $cell; + } + $vals = []; + $temp = array_combine($header, $row); + foreach ($temp as $k => $v) { + if (isset($fieldArr[$k]) && '' !== $k) { + $vals[$fieldArr[$k]] = $v; + } + } + $insert[] = $vals; + } + $currentIndex++; } } - if ($row) { - $insert[] = $row; - } + + $chunkSheet->removeSheetByIndex(0); + $chunkSheet->createSheet(0); + } + } catch (Exception $exception) { - $this->error($exception->getMessage()); + $msg = $exception->getMessage(); + if (preg_match("/.*The filename (.+) is not recognised as an OLE file.*/is", $msg, $matches)) { + $msg = "读取文件失败, 建议重新将文件另存为csv、xls或xslx格式"; + }; + $this->error($msg); } + if (!$insert) { $this->error(__('No rows were updated')); } - try { - //是否包含admin_id字段 - $has_admin_id = false; - foreach ($fieldArr as $name => $key) { - if ($key == 'admin_id') { - $has_admin_id = true; - break; + //是否包含admin_id字段 + + $has_admin_id = false; + foreach ($fieldArr as $name => $key) { + if ('admin_id' == $key) { + $has_admin_id = true; + break; + } + } + + if ($has_admin_id) { + $auth = Auth::instance(); + foreach ($insert as &$val) { + if (!isset($val['admin_id']) || empty($val['admin_id'])) { + $val['admin_id'] = $auth->isLogin() ? $auth->id : 0; } } - if ($has_admin_id) { - $auth = Auth::instance(); - foreach ($insert as &$val) { - if (!isset($val['admin_id']) || empty($val['admin_id'])) { - $val['admin_id'] = $auth->isLogin() ? $auth->id : 0; - } + } + + // 批量插入数据 + + $affectRows = 0; + $repeatRows = 0; + + foreach ($insert as &$data) { + + $errMsg = null; + $error = null; + $event = null; + + $hook = [ + 'event' => 'before_import', + 'class' => static::class, + 'model' => $model, + 'data' => $data, + 'error' => null, + ]; + + Hook::listen('table_import', $hook); + + try { + + $affectRows += (new $model($hook['data']))->save(); + $event = 'after_import'; + + } catch (PDOException $e) { + + $error = $e; + $event = 'pdo_exception'; + + if (preg_match("/.+Integrity constraint violation: 1062 Duplicate entry '(.+)' for key '(.+)'/is", $error, $matches)) { + $errMsg = "导入失败,包含【{$matches[1]}】的记录已存在"; + $event = 'pdo_duplicate_entry'; + $repeatRows++; } + + } catch (Exception $e) { + $error = $e; + $event = 'exception'; } - $this->model->saveAll($insert); - } catch (PDOException $exception) { - $msg = $exception->getMessage(); - if (preg_match("/.+Integrity constraint violation: 1062 Duplicate entry '(.+)' for key '(.+)'/is", $msg, $matches)) { - $msg = "导入失败,包含【{$matches[1]}】的记录已存在"; - }; - $this->error($msg); - } catch (Exception $e) { - $this->error($e->getMessage()); + + $hook['event'] = $event; + $hook['error'] = $error; + + Hook::listen('table_import', $hook); + $error = $hook['error']; + + if ($error) { + $errMsg = $errMsg ?: $error->getMessage(); + $this->error($errMsg); + } + } - $this->success(); + if (!$affectRows) { + $this->error(__('No rows were updated')); + } + + $timer2 = microtime(true); + $usage2 = memory_get_usage(); + $usage = round(($usage2 - $usage1) / 1024 / 1024, 2); + $timer = round($timer2 - $timer1, 3); + + $success = vsprintf("导入成功!
%s
%s
%s
%s", [ + sprintf("新增数据: %d 条", $affectRows), + sprintf("重复数据: %d 条", $repeatRows), + sprintf("内存占用: %s MB", $usage), + sprintf("总共耗时: %s 秒", $timer), + ]); + + $this->success($success); } }