From 9c81113fa7cbe46c3c298ced6a391b1d3865771e Mon Sep 17 00:00:00 2001 From: jsasg <735273025@qq.com> Date: Wed, 12 Aug 2026 14:53:03 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BA=A7=E5=93=81=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E6=80=A7=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/common.php | 78 +++++++++++++++++++++++ app/admin/controller/v1/ProductCompat.php | 22 +++---- 2 files changed, 87 insertions(+), 13 deletions(-) diff --git a/app/admin/common.php b/app/admin/common.php index b0be5f20..85337748 100644 --- a/app/admin/common.php +++ b/app/admin/common.php @@ -148,6 +148,84 @@ if (!function_exists('xlsx_writer')) { } } +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')) { /** diff --git a/app/admin/controller/v1/ProductCompat.php b/app/admin/controller/v1/ProductCompat.php index d7d310f9..11ee6289 100644 --- a/app/admin/controller/v1/ProductCompat.php +++ b/app/admin/controller/v1/ProductCompat.php @@ -318,15 +318,7 @@ class ProductCompat 'language_id', 'deleted_at' ]) - ->with([ - 'types' => function ($query) { - $query->field(['id', 'name']); - }, - 'parts' => function ($query) { - $query->field(['id', 'name']); - } - ]) - ->language(request()->lang_id) + ->language(1) ->where(function ($query) use($params) { if (!empty($params['type_id'])) { $query->typeId($params['type_id']); @@ -341,6 +333,10 @@ class ProductCompat 'compatible_model' => $params['compatible_model'] ?? null, ]); + // 类型/配件为字典小表,一次性载入内存映射,避免每批预加载关联查询 + $type_map = ProductCompatTypesModel::column('name', 'id'); + $part_map = ProductCompatPartsModel::column('name', 'id'); + $schema = [ 'id' => 'id', 'type_name' => '产品类型', @@ -359,9 +355,9 @@ class ProductCompat 'updated_at' => '更新时间' ]; - return xlsx_writer($this->xlsxRowGenerator($query), $schema, '产品兼容性' . date('YmdHis')); + return xlsx_download_xaccel($this->xlsxRowGenerator($query, $type_map, $part_map), $schema, '产品兼容性' . date('YmdHis')); } - private function xlsxRowGenerator(Query $query, int $page_size = 500): Generator + private function xlsxRowGenerator(Query $query, array $type_map, array $part_map, int $page_size = 500): Generator { // 主键游标分页,避免 offset 深翻页的性能问题 $last_id = 0; @@ -392,10 +388,10 @@ class ProductCompat foreach ($items as $item) { yield [ 'id' => $item['id'], - 'type_name' => $item['types']['name'] ?? '', + 'type_name' => $type_map[$item['type_id']] ?? '', 'brand_name' => $item['brand_name'], 'model' => $item['model'], - 'part_name' => $item['parts']['name'] ?? '', + 'part_name' => $part_map[$item['part_id']] ?? '', 'level_name' => $item['level_name'], 'spec_name' => $item['spec_name'], 'series_name' => $item['series_name'],