поиск+доставка

This commit is contained in:
Alisa
2026-06-07 13:12:42 +03:00
parent 65d182b4af
commit ee753429e2
20 changed files with 3429 additions and 91 deletions
+91 -3
View File
@@ -9,13 +9,25 @@ use App\Repositories\ProductRepository;
final class CatalogController
{
private ?string $searchBasePath = null;
public function index(): void
{
$categoryRepository = new CategoryRepository();
$productRepository = new ProductRepository();
$pagination = $this->paginationInput();
$totalProducts = $productRepository->catalogTotal(true);
$products = $productRepository->catalogPreview($pagination['limit'], $pagination['offset'], true);
$searchQuery = trim((string) ($_GET['q'] ?? $this->queryParameter('q')));
$basePath = '/catalog';
if ($searchQuery !== '') {
$searchResult = $this->searchProducts($productRepository, $searchQuery, $pagination['limit'], $pagination['offset']);
$totalProducts = $searchResult['total'];
$products = $searchResult['products'];
$basePath = $this->searchBasePath ?? ($basePath . '?q=' . rawurlencode($searchQuery));
} else {
$totalProducts = $productRepository->catalogTotal(true);
$products = $productRepository->catalogPreview($pagination['limit'], $pagination['offset'], true);
}
view('catalog/index', [
'title' => 'Каталог Рыбсток - рыба, морепродукты, мясо, сыры и продукты',
@@ -28,10 +40,18 @@ final class CatalogController
'categories' => $categoryRepository->tree(),
'currentCategory' => null,
'products' => $products,
'pagination' => $this->paginationData('/catalog', $totalProducts, count($products), $pagination),
'searchQuery' => $searchQuery,
'pagination' => $this->paginationData($basePath, $totalProducts, count($products), $pagination),
]);
}
public function search(string $query): void
{
$_GET['q'] = trim($query);
$this->searchBasePath = '/search/' . rawurlencode(trim($query));
$this->index();
}
public function category(string $slug): void
{
$categoryRepository = new CategoryRepository();
@@ -140,6 +160,74 @@ final class CatalogController
];
}
private function queryParameter(string $name): string
{
$query = (string) (parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_QUERY) ?? '');
if ($query === '') {
return '';
}
parse_str($query, $parameters);
return (string) ($parameters[$name] ?? '');
}
/**
* @return array{products: array<int, array<string, mixed>>, total: int}
*/
private function searchProducts(ProductRepository $productRepository, string $query, int $limit, int $offset): array
{
if (method_exists($productRepository, 'searchCatalog')) {
return $productRepository->searchCatalog($query, $limit, $offset, true);
}
$needle = $this->lower($query);
$words = array_values(array_filter(preg_split('/\s+/u', $needle) ?: []));
$ranked = [];
foreach ($productRepository->catalogPreview(5000, 0, true) as $product) {
$haystack = $this->lower(implode(' ', [
(string) ($product['name'] ?? ''),
(string) ($product['short_description'] ?? ''),
(string) ($product['category_name'] ?? ''),
(string) ($product['category_display_name'] ?? ''),
]));
$score = 0;
if ($needle !== '' && str_contains($haystack, $needle)) {
$score += 100;
}
foreach ($words as $word) {
if ($word !== '' && str_contains($haystack, $word)) {
$score += 20;
}
}
if ($score > 0) {
$product['_search_score'] = $score;
$ranked[] = $product;
}
}
usort($ranked, static fn (array $left, array $right): int => ($right['_search_score'] ?? 0) <=> ($left['_search_score'] ?? 0));
foreach ($ranked as &$product) {
unset($product['_search_score']);
}
unset($product);
return [
'products' => array_slice($ranked, $offset, $limit),
'total' => count($ranked),
];
}
private function lower(string $value): string
{
return function_exists('mb_strtolower') ? mb_strtolower($value, 'UTF-8') : strtolower($value);
}
/**
* @param array{page:int, limit:int, offset:int, per_page:int, cumulative:bool} $input
* @return array<string, mixed>
+181
View File
@@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Core\Database;
use App\Repositories\SettingRepository;
use Throwable;
final class PriceRequestController
{
public function store(): void
{
$companyName = trim((string) ($_POST['company_name'] ?? ''));
$paymentType = trim((string) ($_POST['payment_type'] ?? ''));
$phone = trim((string) ($_POST['phone'] ?? ''));
$email = trim((string) ($_POST['email'] ?? ''));
$activityType = trim((string) ($_POST['activity_type'] ?? ''));
$requestText = trim((string) ($_POST['request_text'] ?? ''));
$errors = [];
if ($companyName === '') {
$errors[] = 'company';
}
if (!in_array($paymentType, ['cash', 'invoice'], true)) {
$errors[] = 'payment';
}
if ($phone === '' && $email === '') {
$errors[] = 'contact';
}
if ($requestText === '') {
$errors[] = 'request';
}
if ($errors !== []) {
$this->redirect('error');
}
try {
$uploadedFiles = $this->storeUploadedFiles();
$fullRequestText = $this->buildRequestText($paymentType, $activityType, $requestText, $uploadedFiles);
$statement = Database::pdo()->prepare(
'INSERT INTO price_requests
(customer_name, phone, email, company_name, request_text, file_path, status, client_ip, user_agent)
VALUES
(:customer_name, :phone, :email, :company_name, :request_text, :file_path, :status, :client_ip, :user_agent)'
);
$statement->execute([
'customer_name' => $companyName,
'phone' => $phone !== '' ? $phone : null,
'email' => $email !== '' ? $email : null,
'company_name' => $companyName,
'request_text' => $fullRequestText,
'file_path' => $uploadedFiles[0] ?? null,
'status' => 'new',
'client_ip' => $_SERVER['REMOTE_ADDR'] ?? null,
'user_agent' => substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255),
]);
$requestId = (int) Database::pdo()->lastInsertId();
$this->sendNotification($requestId, $companyName, $phone, $email, $fullRequestText);
} catch (Throwable) {
$this->redirect('error');
}
$this->redirect('sent');
}
/**
* @return array<int, string>
*/
private function storeUploadedFiles(): array
{
if (!isset($_FILES['request_files']) || !is_array($_FILES['request_files']['name'])) {
return [];
}
$stored = [];
$uploadDir = base_path('storage/price_requests/' . date('Y/m'));
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
}
foreach ($_FILES['request_files']['name'] as $index => $originalName) {
$error = (int) ($_FILES['request_files']['error'][$index] ?? UPLOAD_ERR_NO_FILE);
if ($error === UPLOAD_ERR_NO_FILE) {
continue;
}
if ($error !== UPLOAD_ERR_OK) {
continue;
}
$tmpName = (string) ($_FILES['request_files']['tmp_name'][$index] ?? '');
if ($tmpName === '' || !is_uploaded_file($tmpName)) {
continue;
}
$extension = strtolower(pathinfo((string) $originalName, PATHINFO_EXTENSION));
$safeExtension = preg_replace('/[^a-z0-9]/', '', $extension) ?: 'file';
$fileName = date('Ymd-His') . '-' . bin2hex(random_bytes(4)) . '.' . $safeExtension;
$targetPath = $uploadDir . '/' . $fileName;
if (move_uploaded_file($tmpName, $targetPath)) {
$stored[] = str_replace('\\', '/', substr($targetPath, strlen(base_path()) + 1));
}
}
return $stored;
}
/**
* @param array<int, string> $uploadedFiles
*/
private function buildRequestText(string $paymentType, string $activityType, string $requestText, array $uploadedFiles): string
{
$paymentLabel = $paymentType === 'invoice' ? 'Расчетный счет' : 'Наличные';
$lines = [
'Форма оплаты: ' . $paymentLabel,
'Тип деятельности: ' . ($activityType !== '' ? $activityType : 'не указан'),
'',
'Позиции, объем и нужная цена:',
$requestText,
];
if ($uploadedFiles !== []) {
$lines[] = '';
$lines[] = 'Файлы:';
foreach ($uploadedFiles as $path) {
$lines[] = '- ' . $path;
}
}
return implode("\n", $lines);
}
private function sendNotification(int $requestId, string $companyName, string $phone, string $email, string $requestText): void
{
$settings = (new SettingRepository())->allKeyed();
$to = trim((string) (($settings['price_request_email'] ?? '') ?: ($settings['admin_email'] ?? '') ?: 'zakaz@rybstock.ru'));
if ($to === '') {
return;
}
$subject = 'Запрос проходных цен #' . $requestId . ' - ' . $companyName;
$body = implode("\n", [
'Новая бизнес-заявка на проходные цены.',
'',
'Номер заявки: ' . $requestId,
'Организация: ' . $companyName,
'Телефон: ' . ($phone !== '' ? $phone : 'не указан'),
'Email: ' . ($email !== '' ? $email : 'не указан'),
'',
$requestText,
]);
$headers = [
'Content-Type: text/plain; charset=UTF-8',
'From: Рыбсток <no-reply@rybstock.ru>',
];
@mail($to, $subject, $body, implode("\r\n", $headers));
}
private function redirect(string $status): never
{
header('Location: /?price_request=' . rawurlencode($status) . '#price-request', true, 303);
exit;
}
}