шаг 5 завершен

This commit is contained in:
Alisa
2026-06-04 19:19:35 +03:00
parent 238831ad3a
commit 6216c1ee4f
34 changed files with 1859 additions and 36 deletions
+8 -6
View File
@@ -15,9 +15,9 @@ final class CatalogController
$productRepository = new ProductRepository();
view('catalog/index', [
'title' => 'Каталог продукции - Рыбсток',
'metaDescription' => 'Полный каталог Рыбсток: рыба, морепродукты, икра, мясо, сыры, полуфабрикаты и продукты с доставкой.',
'metaKeywords' => 'каталог Рыбсток, рыба, морепродукты, мясо, сыры, полуфабрикаты',
'title' => 'Каталог Рыбсток - рыба, морепродукты, мясо, сыры и продукты',
'metaDescription' => 'Полный каталог Рыбсток: рыба, морепродукты, икра, мясо, сыры, полуфабрикаты, овощи, ягоды и бакалея по стоковым ценам с доставкой по Москве и Московской области.',
'metaKeywords' => 'каталог Рыбсток, купить рыбу Москва, морепродукты Москва, мясо доставка Москва, сыры доставка, икра, полуфабрикаты, овощи ягоды заморозка, бакалея, продукты по стоковым ценам',
'breadcrumbs' => [
['title' => 'Главная', 'url' => '/'],
['title' => 'Каталог'],
@@ -47,10 +47,12 @@ final class CatalogController
return;
}
$categoryName = (string) $category['name'];
view('catalog/index', [
'title' => ($category['seo_title'] ?: $category['name']) . ' - Рыбсток',
'metaDescription' => $category['seo_description'] ?: '',
'metaKeywords' => $category['seo_keywords'] ?: '',
'title' => $category['seo_title'] ?: $categoryName . ' - купить в Рыбсток с доставкой по Москве',
'metaDescription' => $category['seo_description'] ?: $categoryName . ' в интернет-складе Рыбсток: качественная продукция по стоковым ценам, фасовки для дома и бизнеса, доставка по Москве и Московской области.',
'metaKeywords' => $category['seo_keywords'] ?: $categoryName . ', купить ' . $categoryName . ' Москва, ' . $categoryName . ' доставка, Рыбсток, продукты по стоковым ценам',
'breadcrumbs' => $categoryRepository->breadcrumbs((int) $category['id']),
'categories' => $categoryRepository->tree(),
'currentCategory' => $category,
+3 -3
View File
@@ -38,9 +38,9 @@ final class HomeController
}
view('pages/home', [
'title' => 'Рыбсток - продукты и рыба с доставкой по Москве и Московской области',
'metaDescription' => 'Рыбсток - интернет-магазин рыбы, морепродуктов, мяса, сыров, полуфабрикатов и продуктов с доставкой по Москве и Московской области.',
'metaKeywords' => 'Рыбсток, рыба, морепродукты, мясо, сыры, полуфабрикаты, доставка продуктов Москва',
'title' => 'Рыбсток - качественные продукты по стоковым ценам с доставкой',
'metaDescription' => 'Рыбсток - интернет-склад качественной рыбы, морепродуктов, мяса, сыров, овощей, ягод и бакалеи по стоковым ценам. Бесплатная доставка по Москве и Московской области, самовывоз с ул. Привольная.',
'metaKeywords' => 'Рыбсток, рыба доставка Москва, морепродукты доставка Москва, мясо доставка Москва, сыры доставка, икра доставка Москва, овощи ягоды заморозка, бакалея с доставкой, продукты по стоковым ценам, интернет склад продуктов, бесплатная доставка продуктов Москва, доставка продуктов Московская область',
'breadcrumbs' => [
['title' => 'Главная'],
],
+60
View File
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
final class PageController
{
public function delivery(): void
{
view('pages/delivery', [
'title' => 'Доставка Рыбсток - Москва и Московская область',
'metaDescription' => 'Условия доставки Рыбсток по Москве и Московской области: бесплатная доставка внутри МКАД от 5000 рублей, за МКАД от 10000 рублей, самовывоз и расчет доставки.',
'metaKeywords' => 'Рыбсток доставка, доставка рыбы Москва, доставка продуктов Москва, доставка за МКАД, самовывоз Рыбсток, расчет доставки',
'breadcrumbs' => [
['title' => 'Главная', 'url' => '/'],
['title' => 'Доставка'],
],
]);
}
public function payment(): void
{
view('pages/payment', [
'title' => 'Оплата заказа - Рыбсток',
'metaDescription' => 'Способы оплаты заказа в Рыбсток: оплата при получении наличными или QR-кодом, безналичный расчет от 30000 рублей.',
'metaKeywords' => 'Рыбсток оплата, оплата при получении, оплата QR кодом, безналичный расчет продукты',
'breadcrumbs' => [
['title' => 'Главная', 'url' => '/'],
['title' => 'Оплата'],
],
]);
}
public function contacts(): void
{
view('pages/contacts', [
'title' => 'Контакты Рыбсток - телефон, адрес самовывоза, email',
'metaDescription' => 'Контакты Рыбсток: телефоны для заказа и консультации, адрес пункта самовывоза в Москве на ул. Привольная, д. 13к1, email для заявок.',
'metaKeywords' => 'Рыбсток контакты, телефон Рыбсток, адрес самовывоза Рыбсток, zakaz@rybstock.ru',
'breadcrumbs' => [
['title' => 'Главная', 'url' => '/'],
['title' => 'Контакты'],
],
]);
}
public function returns(): void
{
view('pages/returns', [
'title' => 'Правила возврата товара - Рыбсток',
'metaDescription' => 'Правила возврата товара в интернет-складе Рыбсток: условия возврата, сохранность упаковки, температурный режим и порядок обращения.',
'metaKeywords' => 'Рыбсток возврат товара, правила возврата товара, возврат продуктов, условия возврата Рыбсток',
'breadcrumbs' => [
['title' => 'Главная', 'url' => '/'],
['title' => 'Правила возврата товара'],
],
]);
}
}
+86
View File
@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace App\Core;
use App\Repositories\AdminRepository;
final class Auth
{
private const SESSION_ADMIN_ID = 'admin_id';
public static function start(): void
{
if (session_status() === PHP_SESSION_ACTIVE) {
return;
}
session_name('rybstock_admin');
session_start();
}
public static function requireAdmin(): void
{
self::start();
if (self::admin() !== null) {
return;
}
$next = $_SERVER['REQUEST_URI'] ?? '/admin/';
header('Location: /admin/login.php?next=' . rawurlencode($next));
exit;
}
/**
* @return array<string, mixed>|null
*/
public static function admin(): ?array
{
self::start();
$adminId = (int) ($_SESSION[self::SESSION_ADMIN_ID] ?? 0);
if ($adminId <= 0) {
return null;
}
$admin = (new AdminRepository())->findActiveById($adminId);
if ($admin === null) {
unset($_SESSION[self::SESSION_ADMIN_ID]);
}
return $admin;
}
public static function attempt(string $email, string $password): bool
{
self::start();
$repository = new AdminRepository();
$admin = $repository->findActiveByEmail($email);
if ($admin === null || !password_verify($password, (string) $admin['password_hash'])) {
return false;
}
session_regenerate_id(true);
$_SESSION[self::SESSION_ADMIN_ID] = (int) $admin['id'];
$repository->touchLastLogin((int) $admin['id']);
return true;
}
public static function logout(): void
{
self::start();
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], (bool) $params['secure'], (bool) $params['httponly']);
}
session_destroy();
}
}
+87
View File
@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
final class AdminRepository extends BaseRepository
{
public function activeCount(): int
{
return (int) $this->pdo()
->query('SELECT COUNT(*) FROM admins WHERE is_active = 1')
->fetchColumn();
}
/**
* @return array<string, mixed>|null
*/
public function findActiveByEmail(string $email): ?array
{
$statement = $this->pdo()->prepare(
'SELECT id, name, email, password_hash, role
FROM admins
WHERE email = :email AND is_active = 1
LIMIT 1'
);
$statement->execute(['email' => mb_strtolower(trim($email))]);
$admin = $statement->fetch();
return $admin === false ? null : $admin;
}
/**
* @return array<string, mixed>|null
*/
public function findActiveById(int $id): ?array
{
$statement = $this->pdo()->prepare(
'SELECT id, name, email, role
FROM admins
WHERE id = :id AND is_active = 1
LIMIT 1'
);
$statement->execute(['id' => $id]);
$admin = $statement->fetch();
return $admin === false ? null : $admin;
}
public function createOwner(string $name, string $email, string $password): int
{
$name = trim($name);
$email = mb_strtolower(trim($email));
if ($name === '' || $email === '' || $password === '') {
throw new \InvalidArgumentException('Имя, email и пароль обязательны.');
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Укажите корректный email.');
}
if (mb_strlen($password) < 8) {
throw new \InvalidArgumentException('Пароль должен быть не короче 8 символов.');
}
$statement = $this->pdo()->prepare(
'INSERT INTO admins (name, email, password_hash, role, is_active)
VALUES (:name, :email, :password_hash, "owner", 1)'
);
$statement->execute([
'name' => $name,
'email' => $email,
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
]);
return (int) $this->pdo()->lastInsertId();
}
public function touchLastLogin(int $id): void
{
$statement = $this->pdo()->prepare(
'UPDATE admins SET last_login_at = NOW() WHERE id = :id'
);
$statement->execute(['id' => $id]);
}
}
+9
View File
@@ -38,6 +38,15 @@ final class PriceTierService
];
}
$tiers[] = [
'label' => 'Безналичный расчет',
'code' => null,
'min_order_amount' => 0.0,
'discount_percent' => -8.0,
'price' => round($basePrice * 1.08, 2),
'note' => '+8% к базовой цене при безналичном расчете',
];
return $tiers;
}