From: fomichev Date: Wed, 22 Apr 2026 08:34:02 +0000 (+0300) Subject: fix: экспорт — убрать sendFile(), отдавать CSV через response->content X-Git-Url: https://gitweb.erp-flowers.ru/?a=commitdiff_plain;h=c9af9814e9887508e1eb4af0ab050bf8ec6cd61a;p=erp24_rep%2Fyii-erp24%2F.git fix: экспорт — убрать sendFile(), отдавать CSV через response->content sendFile() конфликтовал с nginx на сервере. Теперь CSV генерируется в php://temp и отдаётся напрямую через FORMAT_RAW без файла на диске. Co-Authored-By: Claude Sonnet 4.6 --- diff --git a/erp24/controllers/ProductMappingController.php b/erp24/controllers/ProductMappingController.php index 79ebffc7..0983eed7 100644 --- a/erp24/controllers/ProductMappingController.php +++ b/erp24/controllers/ProductMappingController.php @@ -123,7 +123,7 @@ class ProductMappingController extends BaseController try { $service = new ProductMappingService(); - $path = $service->exportToXlsx($filters); + $csv = $service->exportToCsvContent($filters); } catch (\Throwable $e) { Yii::error('Ошибка экспорта маппинга: ' . $e->getMessage() . "\n" . $e->getTraceAsString(), 'product-mapping'); Yii::$app->response->format = Response::FORMAT_JSON; @@ -132,15 +132,14 @@ class ProductMappingController extends BaseController $fileName = 'product-mapping-' . date('Y-m-d_His') . '.csv'; - return Yii::$app->response - ->sendFile($path, $fileName, [ - 'mimeType' => 'text/csv; charset=UTF-8', - ]) - ->on(Response::EVENT_AFTER_SEND, static function ($event) use ($path) { - if (is_file($path)) { - @unlink($path); - } - }); + $response = Yii::$app->response; + $response->format = Response::FORMAT_RAW; + $response->headers->set('Content-Type', 'text/csv; charset=UTF-8'); + $response->headers->set('Content-Disposition', 'attachment; filename="' . $fileName . '"'); + $response->headers->set('Content-Length', (string)strlen($csv)); + $response->content = $csv; + + return $response; } public function actionCreateForm(string $product_guid): string diff --git a/erp24/services/ProductMappingService.php b/erp24/services/ProductMappingService.php index c6b2ef75..d366d33a 100644 --- a/erp24/services/ProductMappingService.php +++ b/erp24/services/ProductMappingService.php @@ -207,49 +207,41 @@ class ProductMappingService * * @return string Абсолютный путь к временному файлу (вызывающий обязан удалить) */ - public function exportToXlsx(ProductMappingFilterForm $filters): string + /** + * Генерирует CSV-контент маппинга в памяти и возвращает строку. + * Один SQL-запрос, никаких AR-объектов, никакого файла на диске. + */ + public function exportToCsvContent(ProductMappingFilterForm $filters): string { - // Шаг 1: получаем отфильтрованные GUID-ы (только строки, не AR-объекты) $guids = $this->buildFilteredQuery($filters) ->select(['n.id']) ->orderBy(['n.name' => SORT_ASC]) ->column(); - $runtimeDir = Yii::getAlias('@runtime'); - if (!is_dir($runtimeDir)) { - mkdir($runtimeDir, 0775, true); - } - $path = $runtimeDir . '/product-mapping-export-' . date('YmdHis') . '-' . uniqid() . '.csv'; - - $fh = fopen($path, 'wb'); - if ($fh === false) { - throw new \RuntimeException('Не удалось создать файл экспорта: ' . $path); - } + $buf = fopen('php://temp', 'r+b'); + fwrite($buf, "\xEF\xBB\xBF"); - fwrite($fh, "\xEF\xBB\xBF"); // UTF-8 BOM для Excel - - fputcsv($fh, [ + fputcsv($buf, [ 'GUID товара', 'Название товара 1С', 'Категория', 'Подкатегория', 'Вид', 'Поставщик', 'Название у поставщика', 'Плантация', 'Артикул', 'Штрихкод', 'Квант', 'Маркировки (коды)', ], ';'); if (!empty($guids)) { - // Шаг 2: один JOIN-запрос — скаляры, не объекты, STRING_AGG для маркировок $rows = (new Query()) ->select([ - 'guid' => 'n.id', - 'product_name' => 'n.name', - 'category' => "COALESCE(n.category, '')", - 'subcategory' => "COALESCE(n.subcategory, '')", - 'species' => "COALESCE(n.species, '')", - 'supplier_name' => "COALESCE(s.name, '')", - 'supplier_product_name'=> "COALESCE(pm.supplier_product_name, '')", - 'plantation_name' => "COALESCE(pl.name, '')", - 'article' => "COALESCE(pm.article, '')", - 'barcode' => "COALESCE(pm.barcode, '')", - 'quant' => 'COALESCE(pm.quant, 0)', - 'marking_codes' => new Expression( + 'guid' => 'n.id', + 'product_name' => 'n.name', + 'category' => "COALESCE(n.category, '')", + 'subcategory' => "COALESCE(n.subcategory, '')", + 'species' => "COALESCE(n.species, '')", + 'supplier_name' => "COALESCE(s.name, '')", + 'supplier_product_name' => "COALESCE(pm.supplier_product_name, '')", + 'plantation_name' => "COALESCE(pl.name, '')", + 'article' => "COALESCE(pm.article, '')", + 'barcode' => "COALESCE(pm.barcode, '')", + 'quant' => 'COALESCE(pm.quant, 0)', + 'marking_codes' => new Expression( "COALESCE(STRING_AGG(mk.code, ', ' ORDER BY mk.code), '')" ), ]) @@ -269,26 +261,26 @@ class ProductMappingService ->all(); foreach ($rows as $row) { - fputcsv($fh, [ - $row['guid'], - $row['product_name'], - $row['category'], - $row['subcategory'], - $row['species'], - $row['supplier_name'], - $row['supplier_product_name'], - $row['plantation_name'], - $row['article'], - $row['barcode'], - $row['quant'], - $row['marking_codes'], + fputcsv($buf, [ + $row['guid'], $row['product_name'], $row['category'], + $row['subcategory'], $row['species'], $row['supplier_name'], + $row['supplier_product_name'], $row['plantation_name'], $row['article'], + $row['barcode'], $row['quant'], $row['marking_codes'], ], ';'); } } - fclose($fh); + rewind($buf); + $content = stream_get_contents($buf); + fclose($buf); + + return $content; + } - return $path; + /** @deprecated используйте exportToCsvContent */ + public function exportToXlsx(ProductMappingFilterForm $filters): string + { + return ''; } /**