键 例:['A' => 'name', 'B' => 'age', 'C' => 'sex'] * @return array */ function xlsx_reader(string $file, int $initial_row = 1, array $keys_map = [], bool $read_only = false): array { $reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader('Xlsx'); if ($read_only) { // 只读模式,不读取公式,样式等等,减少内存消耗 $reader->setReadDataOnly(true); } // 读取文件 $spreadsheet = $reader->load($file); // 获取活动sheet $sheet = $spreadsheet->getActiveSheet(); // 获取最大行 $max_row = $sheet->getHighestRow(); // 获取最大列 $max_col = $sheet->getHighestColumn(); $xlsx_data = []; $first_col = ord('A'); $last_col = ord($max_col); for ($row = $initial_row; $row <= $max_row; $row++) { for ($col = $first_col; $col <= $last_col; $col++) { $key = $col_chr = chr($col); if (!empty($keys_map)) { $key = $keys_map[$col_chr] ?? $col_chr; } $xlsx_data[$row][$key] = $sheet->getCell($col_chr . $row)->getValue(); } } return $xlsx_data; } } if (!function_exists('xlsx_stream_reader')) { /** * 读取Excel数据 * @param string $file Excel文件路径 * @param int $initial_row 初始行 * @param array $keys_map 列 -> 键 例:['A' => 'name', 'B' => 'age', 'C' => 'sex'] * @return Generator */ function xlsx_stream_reader(string $file, int $initial_row = 1, array $keys_map = [], bool $read_only = false): Generator { $reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader('Xlsx'); if ($read_only) { // 只读模式,不读取公式,样式等等,减少内存消耗 $reader->setReadDataOnly(true); } // 读取文件 $spreadsheet = $reader->load($file); // 获取活动sheet $sheet = $spreadsheet->getActiveSheet(); // 获取最大行 $max_row = $sheet->getHighestRow(); // 获取最大列 $max_col = $sheet->getHighestColumn(); $xlsx_row = []; $first_col = ord('A'); $last_col = ord($max_col); for ($row = $initial_row; $row <= $max_row; $row++) { for ($col = $first_col; $col <= $last_col; $col++) { $key = $col_chr = chr($col); if (!empty($keys_map)) { $key = $keys_map[$col_chr] ?? $col_chr; } $xlsx_row[$key] = $sheet->getCell($col_chr . $row)->getValue(); } yield $xlsx_row; } // 主动销毁对象以释放内存 $spreadsheet->disconnectWorksheets(); unset($spreadsheet); } } if (!function_exists('xlsx_writer')) { /** * 构建Excel数据 * @param array $data 数据 例:[['name'=>'张三','age'=>18, 'sex'=>'男']] * @param array $schema 表头信息 例:['name' => '姓名', 'age' => '年龄', 'sex' => '性别'] * @param string $filename 下载文件名称 默认为YmdHis时间 * @param string $output 输出方式 php://output 或 文件路径 * @return \think\Response */ function xlsx_writer(array|\think\model\Collection|Generator $data, array $schema, string $filename = '', string $output = 'php://output'): \think\Response { // 获取Spreadsheet对象 $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); // 写入表头 $title = array_values($schema); $title_col = 'A'; foreach ($title as $value) { // 单元格内容写入 $sheet->setCellValue($title_col . '1', $value); $title_col++; } // 写入数据 $row = 2; $keys = array_keys($schema); foreach ($data as $item) { $data_col = 'A'; foreach ($keys as $key) { $sheet->setCellValue($data_col . $row, $item[$key] ?? ''); $data_col++; } $row++; } flush(); ob_flush(); if ($filename == '') $filename = date('YmdHis'); $filename = urlencode($filename); // 文件写入缓冲区 \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xlsx')->save($output); // 从缓冲区获取导出文件,并通过Response返回(解决thinkphp框架的Response无法获取设置的header问题) $file_data = ob_get_clean(); $response = \think\Response::create($file_data)->header([ 'Access-Control-Expose-Headers' => 'Content-Disposition', 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=UTF-8', 'Content-Disposition' => 'attachment;filename=' . $filename . '.xlsx', 'Cache-Control' => 'max-age=0' ]); return $response; } } if (!function_exists('xlsx_download_xaccel')) { /** * 构建Excel数据(采用Nginx Sendfile方式) * * Nginx中需配置 * location /download/ { * internal; # 关键:禁止外部直接访问这个路径 * alias /tmp/; # 使用 alias,将 URI 映射到你存放文件的真实目录 * } * * 注意:避免临时文件造成磁盘堆积,需依赖系统层定时清理(如 systemd-tmpfiles / cron),PHP 侧不做删除 * 如:# 每分钟执行一次,删除 /tmp 下 10 分钟前创建的 xlsx_ 临时文件 * * * * * * find /tmp -maxdepth 1 -name 'ow_xlsx_*' -type f -mmin +10 -delete * * @param array|\think\model\Collection|Generator $data 数据 例:[['name'=>'张三','age'=>18, 'sex'=>'男']] * @param array $schema 表头信息 例:['name' => '姓名', 'age' => '年龄', 'sex' => '性别'] * @param string $filename 下载文件名称 默认为YmdHis时间 * @return \think\Response */ function xlsx_download_xaccel(array|\think\model\Collection|Generator $data, array $schema, string $filename = ''): \think\Response { // 生成到临时文件,避免将整个文件内容写入输出缓冲区 $tmp_file = tempnam(sys_get_temp_dir(), 'ow_xlsx_'); if (false === $tmp_file) { throw new \RuntimeException('创建临时文件失败'); } // tempnam 默认 0600,需放开读权限,否则 Nginx 与 PHP-FPM 用户不同时无法读取 chmod($tmp_file, 0644); // 获取Spreadsheet对象 $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); // 写入表头 $title_col = 'A'; foreach (array_values($schema) as $value) { $sheet->setCellValue($title_col . '1', $value); $title_col++; } // 写入数据 $row = 2; $keys = array_keys($schema); foreach ($data as $item) { $data_col = 'A'; foreach ($keys as $key) { $sheet->setCellValue($data_col . $row, $item[$key] ?? ''); $data_col++; } $row++; } // 写入临时文件,不预计算公式以降低耗时与内存 $writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet); $writer->setPreCalculateFormulas(false); $writer->save($tmp_file); // 释放Spreadsheet内存 $spreadsheet->disconnectWorksheets(); unset($spreadsheet, $writer, $sheet); if ('' == $filename) $filename = date('YmdHis'); $filename = urlencode($filename); $response = \think\Response::create() ->header([ 'Access-Control-Expose-Headers' => 'Content-Disposition', 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=UTF-8', 'Content-Disposition' => 'attachment;filename=' . $filename . '.xlsx', 'Cache-Control' => 'max-age=0', 'Content-Length' => filesize($tmp_file), 'X-Accel-Redirect' => '/download/' . basename($tmp_file), // Nginx ]); return $response; } } if (!function_exists('strtobytes')) { /** * 字符串转字节 * @param string $str_size 字符串 例:'1MB' '2GB' * @return int */ function strtobytes(string $str_size): int { preg_match('/(\d+)(\w+)/', $str_size, $matches); if (!isset($matches[1])) { throw new \Exception('请确认size大小'); } if (!isset($matches[2])) { throw new \Exception('请确认单位'); } $size = intval($matches[1]); $unit = $matches[2]; switch (strtolower($unit)) { case 'kb': $size *= 1024; break; case 'mb': $size *= 1024 * 1024; break; case 'gb': $size *= 1024 * 1024 * 1024; break; case 'tb': $size *= 1024 * 1024 * 1024 * 1024; default: throw new \Exception('不支持[' . $unit . ']单位'); } return $size; } }