шаг 3 завершен
This commit is contained in:
@@ -31,6 +31,15 @@
|
|||||||
1. Установить Open Server Panel, Laragon или другой локальный сервер с PHP и MariaDB.
|
1. Установить Open Server Panel, Laragon или другой локальный сервер с PHP и MariaDB.
|
||||||
2. Направить корень сайта на папку `public/`.
|
2. Направить корень сайта на папку `public/`.
|
||||||
3. Проверить настройки базы в `.env`.
|
3. Проверить настройки базы в `.env`.
|
||||||
4. Открыть сайт в браузере.
|
4. Открыть `/install.php?token=rybstock-local-install` и применить схему базы.
|
||||||
|
5. Открыть сайт в браузере.
|
||||||
|
|
||||||
|
## Диагностика
|
||||||
|
|
||||||
|
Если после изменения файлов сайт показывает старый текст, открыть:
|
||||||
|
|
||||||
|
`/check.php`
|
||||||
|
|
||||||
|
Страница покажет реальный путь исполняемого файла, базу данных, количество таблиц и текущие верхние категории.
|
||||||
|
|
||||||
Если сервер смотрит не в `public/`, нужно настроить виртуальный хост, чтобы публичной была именно эта папка.
|
Если сервер смотрит не в `public/`, нужно настроить виртуальный хост, чтобы публичной была именно эта папка.
|
||||||
|
|||||||
+37
-1
@@ -4,4 +4,40 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once dirname(__DIR__) . '/app/bootstrap.php';
|
require_once dirname(__DIR__) . '/app/bootstrap.php';
|
||||||
|
|
||||||
echo 'RybStock admin panel will be created on the next steps.';
|
$sections = [
|
||||||
|
'Товары',
|
||||||
|
'Категории',
|
||||||
|
'Фасовки',
|
||||||
|
'Заказы',
|
||||||
|
'Промокоды',
|
||||||
|
'Акции',
|
||||||
|
'Популярные товары',
|
||||||
|
'Отзывы',
|
||||||
|
'Настройки',
|
||||||
|
'Прайс-лист',
|
||||||
|
];
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Админка Рыбсток</title>
|
||||||
|
<link rel="stylesheet" href="/assets/css/app.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="page">
|
||||||
|
<section class="start-screen wide">
|
||||||
|
<p class="eyebrow">Админка</p>
|
||||||
|
<h1>Рыбсток</h1>
|
||||||
|
<p>Подготовлены основные разделы будущей панели управления.</p>
|
||||||
|
|
||||||
|
<div class="admin-grid">
|
||||||
|
<?php foreach ($sections as $section): ?>
|
||||||
|
<div class="admin-tile"><?= e($section) ?></div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Core\Database;
|
||||||
|
use App\Repositories\CategoryRepository;
|
||||||
|
use App\Repositories\ProductRepository;
|
||||||
|
use App\Repositories\SettingRepository;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class HomeController
|
||||||
|
{
|
||||||
|
public function index(): void
|
||||||
|
{
|
||||||
|
$dbStatus = 'not_checked';
|
||||||
|
$categories = [];
|
||||||
|
$catalogProducts = [];
|
||||||
|
$popularProducts = [];
|
||||||
|
$settings = [];
|
||||||
|
$notice = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Database::pdo()->query('SELECT 1');
|
||||||
|
$dbStatus = 'connected';
|
||||||
|
|
||||||
|
$categories = (new CategoryRepository())->tree();
|
||||||
|
$productRepository = new ProductRepository();
|
||||||
|
$catalogProducts = $productRepository->catalogPreview(24);
|
||||||
|
$popularProducts = $productRepository->popularPreview(10);
|
||||||
|
$settings = (new SettingRepository())->allKeyed();
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
$dbStatus = app_env('APP_DEBUG', 'false') === 'true'
|
||||||
|
? 'error: ' . $exception->getMessage()
|
||||||
|
: 'error';
|
||||||
|
$notice = 'Если таблицы еще не созданы, откройте страницу установки базы.';
|
||||||
|
}
|
||||||
|
|
||||||
|
view('pages/home', [
|
||||||
|
'title' => 'Рыбсток - продукты и рыба с доставкой по Москве и Московской области',
|
||||||
|
'metaDescription' => 'Рыбсток - интернет-магазин рыбы, морепродуктов, мяса, сыров, полуфабрикатов и продуктов с доставкой по Москве и Московской области.',
|
||||||
|
'metaKeywords' => 'Рыбсток, рыба, морепродукты, мясо, сыры, полуфабрикаты, доставка продуктов Москва',
|
||||||
|
'breadcrumbs' => [
|
||||||
|
['title' => 'Главная'],
|
||||||
|
],
|
||||||
|
'showBackButton' => false,
|
||||||
|
'dbStatus' => $dbStatus,
|
||||||
|
'categories' => $categories,
|
||||||
|
'catalogProducts' => $catalogProducts,
|
||||||
|
'popularProducts' => $popularProducts,
|
||||||
|
'settings' => $settings,
|
||||||
|
'notice' => $notice,
|
||||||
|
'buildVersion' => '2026-06-04-02',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,12 @@ final class View
|
|||||||
|
|
||||||
extract($data, EXTR_SKIP);
|
extract($data, EXTR_SKIP);
|
||||||
|
|
||||||
|
$title = $title ?? 'Рыбсток';
|
||||||
|
$metaDescription = $metaDescription ?? '';
|
||||||
|
$metaKeywords = $metaKeywords ?? '';
|
||||||
|
$breadcrumbs = $breadcrumbs ?? [];
|
||||||
|
$showBackButton = $showBackButton ?? true;
|
||||||
|
|
||||||
require base_path('views/layouts/main.php');
|
require base_path('views/layouts/main.php');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
use App\Core\Database;
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
abstract class BaseRepository
|
||||||
|
{
|
||||||
|
protected function pdo(): PDO
|
||||||
|
{
|
||||||
|
return Database::pdo();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
final class CategoryRepository extends BaseRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function active(): array
|
||||||
|
{
|
||||||
|
$statement = $this->pdo()->query(
|
||||||
|
'SELECT id, parent_id, name, slug, legacy_path, h1, seo_title, seo_description, seo_keywords, image_path, sort_order
|
||||||
|
FROM categories
|
||||||
|
WHERE is_active = 1
|
||||||
|
ORDER BY parent_id IS NOT NULL, sort_order, name'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $statement->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function tree(): array
|
||||||
|
{
|
||||||
|
$items = $this->active();
|
||||||
|
$childrenByParent = [];
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$parentKey = $item['parent_id'] === null ? 0 : (int) $item['parent_id'];
|
||||||
|
$childrenByParent[$parentKey][] = $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->buildTree($childrenByParent, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, array<int, array<string, mixed>>> $childrenByParent
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private function buildTree(array $childrenByParent, int $parentId): array
|
||||||
|
{
|
||||||
|
$branch = [];
|
||||||
|
|
||||||
|
foreach ($childrenByParent[$parentId] ?? [] as $item) {
|
||||||
|
$item['children'] = $this->buildTree($childrenByParent, (int) $item['id']);
|
||||||
|
$branch[] = $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $branch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>|null
|
||||||
|
*/
|
||||||
|
public function findBySlug(string $slug): ?array
|
||||||
|
{
|
||||||
|
$statement = $this->pdo()->prepare(
|
||||||
|
'SELECT id, parent_id, name, slug, legacy_path, description, h1, seo_title, seo_description, seo_keywords, image_path
|
||||||
|
FROM categories
|
||||||
|
WHERE slug = :slug AND is_active = 1
|
||||||
|
LIMIT 1'
|
||||||
|
);
|
||||||
|
$statement->execute(['slug' => $slug]);
|
||||||
|
$category = $statement->fetch();
|
||||||
|
|
||||||
|
return $category ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array<string, string>>
|
||||||
|
*/
|
||||||
|
public function breadcrumbs(int $categoryId): array
|
||||||
|
{
|
||||||
|
$items = [];
|
||||||
|
$currentId = $categoryId;
|
||||||
|
|
||||||
|
while ($currentId > 0) {
|
||||||
|
$statement = $this->pdo()->prepare(
|
||||||
|
'SELECT id, parent_id, name, slug
|
||||||
|
FROM categories
|
||||||
|
WHERE id = :id
|
||||||
|
LIMIT 1'
|
||||||
|
);
|
||||||
|
$statement->execute(['id' => $currentId]);
|
||||||
|
$category = $statement->fetch();
|
||||||
|
|
||||||
|
if (!$category) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
array_unshift($items, [
|
||||||
|
'title' => $category['name'],
|
||||||
|
'url' => '/catalog/' . $category['slug'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$currentId = $category['parent_id'] === null ? 0 : (int) $category['parent_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
array_unshift($items, [
|
||||||
|
'title' => 'Главная',
|
||||||
|
'url' => '/',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
use App\Services\PriceTierService;
|
||||||
|
|
||||||
|
final class ProductRepository extends BaseRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function popularPreview(int $limit = 10): array
|
||||||
|
{
|
||||||
|
$limit = max(1, min($limit, 30));
|
||||||
|
$sql = $this->previewSelectSql(
|
||||||
|
'ORDER BY p.sales_count DESC, RAND()',
|
||||||
|
$limit
|
||||||
|
);
|
||||||
|
|
||||||
|
$statement = $this->pdo()->query($sql);
|
||||||
|
|
||||||
|
return $this->attachPriceTiers($statement->fetchAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function catalogPreview(int $limit = 24): array
|
||||||
|
{
|
||||||
|
$limit = max(1, min($limit, 60));
|
||||||
|
$sql = $this->previewSelectSql(
|
||||||
|
'ORDER BY p.sort_order, p.name',
|
||||||
|
$limit
|
||||||
|
);
|
||||||
|
|
||||||
|
$statement = $this->pdo()->query($sql);
|
||||||
|
|
||||||
|
return $this->attachPriceTiers($statement->fetchAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function previewSelectSql(string $orderBy, int $limit): string
|
||||||
|
{
|
||||||
|
return 'SELECT
|
||||||
|
p.id,
|
||||||
|
p.name,
|
||||||
|
p.slug,
|
||||||
|
p.legacy_path,
|
||||||
|
p.h1,
|
||||||
|
p.seo_title,
|
||||||
|
p.seo_description,
|
||||||
|
p.seo_keywords,
|
||||||
|
c.name AS category_name,
|
||||||
|
c.slug AS category_slug,
|
||||||
|
p.main_image_path,
|
||||||
|
p.base_unit,
|
||||||
|
p.package_display_mode,
|
||||||
|
p.sales_count,
|
||||||
|
v.id AS variant_id,
|
||||||
|
v.name AS variant_name,
|
||||||
|
v.unit,
|
||||||
|
v.package_quantity,
|
||||||
|
v.price_per_unit,
|
||||||
|
v.package_price
|
||||||
|
FROM products p
|
||||||
|
LEFT JOIN categories c ON c.id = p.category_id
|
||||||
|
LEFT JOIN product_variants v
|
||||||
|
ON v.id = (
|
||||||
|
SELECT pv.id
|
||||||
|
FROM product_variants pv
|
||||||
|
WHERE pv.product_id = p.id
|
||||||
|
AND pv.is_published = 1
|
||||||
|
AND pv.is_available = 1
|
||||||
|
ORDER BY pv.is_default DESC, pv.sort_order, pv.id
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
WHERE p.is_published = 1
|
||||||
|
AND p.is_available = 1
|
||||||
|
' . $orderBy . '
|
||||||
|
LIMIT ' . $limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, array<string, mixed>> $products
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private function attachPriceTiers(array $products): array
|
||||||
|
{
|
||||||
|
$priceTierService = new PriceTierService();
|
||||||
|
|
||||||
|
foreach ($products as &$product) {
|
||||||
|
$basePrice = $product['price_per_unit'] ?? $product['package_price'] ?? null;
|
||||||
|
$product['price_tiers'] = $basePrice === null
|
||||||
|
? []
|
||||||
|
: $priceTierService->forAmount((float) $basePrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($product);
|
||||||
|
|
||||||
|
return $products;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
final class ProductVariantRepository extends BaseRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function publishedForProduct(int $productId): array
|
||||||
|
{
|
||||||
|
$statement = $this->pdo()->prepare(
|
||||||
|
'SELECT id, product_id, name, unit, package_quantity, step_quantity, price_per_unit, package_price, is_default
|
||||||
|
FROM product_variants
|
||||||
|
WHERE product_id = :product_id
|
||||||
|
AND is_published = 1
|
||||||
|
AND is_available = 1
|
||||||
|
ORDER BY is_default DESC, sort_order, id'
|
||||||
|
);
|
||||||
|
|
||||||
|
$statement->execute(['product_id' => $productId]);
|
||||||
|
|
||||||
|
return $statement->fetchAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
final class PromocodeRepository extends BaseRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function priceTiers(): array
|
||||||
|
{
|
||||||
|
$statement = $this->pdo()->query(
|
||||||
|
'SELECT code, title, discount_type, discount_value, min_order_amount, price_tier_label, payment_note
|
||||||
|
FROM promocodes
|
||||||
|
WHERE is_active = 1
|
||||||
|
AND show_in_price_tiers = 1
|
||||||
|
AND discount_type = "percent"
|
||||||
|
ORDER BY min_order_amount, discount_value'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $statement->fetchAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
final class SettingRepository extends BaseRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function allKeyed(): array
|
||||||
|
{
|
||||||
|
$statement = $this->pdo()->query(
|
||||||
|
'SELECT setting_key, setting_value, value_type
|
||||||
|
FROM site_settings
|
||||||
|
ORDER BY group_name, setting_key'
|
||||||
|
);
|
||||||
|
|
||||||
|
$settings = [];
|
||||||
|
|
||||||
|
foreach ($statement->fetchAll() as $row) {
|
||||||
|
$settings[$row['setting_key']] = $this->castValue($row['setting_value'], $row['value_type']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function castValue(?string $value, string $type): mixed
|
||||||
|
{
|
||||||
|
return match ($type) {
|
||||||
|
'number' => $value === null ? null : (float) $value,
|
||||||
|
'bool' => $value === '1' || $value === 'true',
|
||||||
|
'json' => $value === null ? null : json_decode($value, true),
|
||||||
|
default => $value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Repositories\PromocodeRepository;
|
||||||
|
|
||||||
|
final class PriceTierService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ?PromocodeRepository $promocodes = null
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function forAmount(float $basePrice): array
|
||||||
|
{
|
||||||
|
$tiers = [[
|
||||||
|
'label' => 'Базовая цена',
|
||||||
|
'min_order_amount' => 0.0,
|
||||||
|
'discount_percent' => 0.0,
|
||||||
|
'price' => round($basePrice, 2),
|
||||||
|
'note' => 'Без промокода',
|
||||||
|
]];
|
||||||
|
|
||||||
|
foreach ($this->repository()->priceTiers() as $promocode) {
|
||||||
|
$discount = (float) $promocode['discount_value'];
|
||||||
|
$tiers[] = [
|
||||||
|
'label' => $promocode['price_tier_label'] ?: $promocode['title'],
|
||||||
|
'code' => $promocode['code'],
|
||||||
|
'min_order_amount' => (float) $promocode['min_order_amount'],
|
||||||
|
'discount_percent' => $discount,
|
||||||
|
'price' => round($basePrice * (100 - $discount) / 100, 2),
|
||||||
|
'note' => $promocode['payment_note'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tiers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function repository(): PromocodeRepository
|
||||||
|
{
|
||||||
|
return $this->promocodes ?? new PromocodeRepository();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,3 +26,8 @@ function view(string $template, array $data = []): void
|
|||||||
{
|
{
|
||||||
View::render($template, $data);
|
View::render($template, $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function e(?string $value): string
|
||||||
|
{
|
||||||
|
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
|||||||
+28
-1
@@ -22,6 +22,7 @@ DROP TABLE IF EXISTS customer_login_codes;
|
|||||||
DROP TABLE IF EXISTS customer_email_tokens;
|
DROP TABLE IF EXISTS customer_email_tokens;
|
||||||
DROP TABLE IF EXISTS site_settings;
|
DROP TABLE IF EXISTS site_settings;
|
||||||
DROP TABLE IF EXISTS holidays;
|
DROP TABLE IF EXISTS holidays;
|
||||||
|
DROP TABLE IF EXISTS redirects;
|
||||||
DROP TABLE IF EXISTS admins;
|
DROP TABLE IF EXISTS admins;
|
||||||
|
|
||||||
CREATE TABLE admins (
|
CREATE TABLE admins (
|
||||||
@@ -43,9 +44,12 @@ CREATE TABLE categories (
|
|||||||
parent_id BIGINT UNSIGNED NULL,
|
parent_id BIGINT UNSIGNED NULL,
|
||||||
name VARCHAR(190) NOT NULL,
|
name VARCHAR(190) NOT NULL,
|
||||||
slug VARCHAR(220) NOT NULL,
|
slug VARCHAR(220) NOT NULL,
|
||||||
|
legacy_path VARCHAR(255) NULL,
|
||||||
description TEXT NULL,
|
description TEXT NULL,
|
||||||
|
h1 VARCHAR(255) NULL,
|
||||||
seo_title VARCHAR(255) NULL,
|
seo_title VARCHAR(255) NULL,
|
||||||
seo_description TEXT NULL,
|
seo_description TEXT NULL,
|
||||||
|
seo_keywords TEXT NULL,
|
||||||
image_path VARCHAR(255) NULL,
|
image_path VARCHAR(255) NULL,
|
||||||
sort_order INT NOT NULL DEFAULT 100,
|
sort_order INT NOT NULL DEFAULT 100,
|
||||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
@@ -53,6 +57,7 @@ CREATE TABLE categories (
|
|||||||
updated_at DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
updated_at DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
UNIQUE KEY uq_categories_slug (slug),
|
UNIQUE KEY uq_categories_slug (slug),
|
||||||
|
UNIQUE KEY uq_categories_legacy_path (legacy_path),
|
||||||
KEY idx_categories_parent (parent_id),
|
KEY idx_categories_parent (parent_id),
|
||||||
KEY idx_categories_active_sort (is_active, sort_order),
|
KEY idx_categories_active_sort (is_active, sort_order),
|
||||||
CONSTRAINT fk_categories_parent
|
CONSTRAINT fk_categories_parent
|
||||||
@@ -65,10 +70,15 @@ CREATE TABLE products (
|
|||||||
category_id BIGINT UNSIGNED NULL,
|
category_id BIGINT UNSIGNED NULL,
|
||||||
name VARCHAR(220) NOT NULL,
|
name VARCHAR(220) NOT NULL,
|
||||||
slug VARCHAR(240) NOT NULL,
|
slug VARCHAR(240) NOT NULL,
|
||||||
|
legacy_path VARCHAR(255) NULL,
|
||||||
sku VARCHAR(100) NULL,
|
sku VARCHAR(100) NULL,
|
||||||
short_description TEXT NULL,
|
short_description TEXT NULL,
|
||||||
description MEDIUMTEXT NULL,
|
description MEDIUMTEXT NULL,
|
||||||
product_info MEDIUMTEXT NULL,
|
product_info MEDIUMTEXT NULL,
|
||||||
|
h1 VARCHAR(255) NULL,
|
||||||
|
seo_title VARCHAR(255) NULL,
|
||||||
|
seo_description TEXT NULL,
|
||||||
|
seo_keywords TEXT NULL,
|
||||||
main_image_path VARCHAR(255) NULL,
|
main_image_path VARCHAR(255) NULL,
|
||||||
base_unit ENUM('kg', 'g', 'l', 'ml', 'pcs', 'pack') NOT NULL DEFAULT 'kg',
|
base_unit ENUM('kg', 'g', 'l', 'ml', 'pcs', 'pack') NOT NULL DEFAULT 'kg',
|
||||||
package_display_mode ENUM('auto', 'base_and_package', 'package_only') NOT NULL DEFAULT 'auto',
|
package_display_mode ENUM('auto', 'base_and_package', 'package_only') NOT NULL DEFAULT 'auto',
|
||||||
@@ -86,6 +96,7 @@ CREATE TABLE products (
|
|||||||
updated_at DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
updated_at DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
UNIQUE KEY uq_products_slug (slug),
|
UNIQUE KEY uq_products_slug (slug),
|
||||||
|
UNIQUE KEY uq_products_legacy_path (legacy_path),
|
||||||
UNIQUE KEY uq_products_sku (sku),
|
UNIQUE KEY uq_products_sku (sku),
|
||||||
KEY idx_products_category (category_id),
|
KEY idx_products_category (category_id),
|
||||||
KEY idx_products_public (is_published, is_available, sort_order),
|
KEY idx_products_public (is_published, is_available, sort_order),
|
||||||
@@ -165,11 +176,14 @@ CREATE TABLE promocodes (
|
|||||||
used_count INT UNSIGNED NOT NULL DEFAULT 0,
|
used_count INT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
is_featured TINYINT(1) NOT NULL DEFAULT 0,
|
is_featured TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
show_in_price_tiers TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
price_tier_label VARCHAR(120) NULL,
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
updated_at DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
UNIQUE KEY uq_promocodes_code (code),
|
UNIQUE KEY uq_promocodes_code (code),
|
||||||
KEY idx_promocodes_active (is_active, starts_at, ends_at)
|
KEY idx_promocodes_active (is_active, starts_at, ends_at),
|
||||||
|
KEY idx_promocodes_price_tiers (show_in_price_tiers, min_order_amount)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
CREATE TABLE promocode_targets (
|
CREATE TABLE promocode_targets (
|
||||||
@@ -368,4 +382,17 @@ CREATE TABLE holidays (
|
|||||||
UNIQUE KEY uq_holidays_date (holiday_date)
|
UNIQUE KEY uq_holidays_date (holiday_date)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE redirects (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
old_path VARCHAR(255) NOT NULL,
|
||||||
|
new_path VARCHAR(255) NOT NULL,
|
||||||
|
http_code SMALLINT UNSIGNED NOT NULL DEFAULT 301,
|
||||||
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uq_redirects_old_path (old_path),
|
||||||
|
KEY idx_redirects_active (is_active)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|||||||
+261
-12
@@ -3,21 +3,270 @@
|
|||||||
|
|
||||||
SET NAMES utf8mb4;
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
INSERT INTO categories (name, slug, sort_order, is_active) VALUES
|
INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES
|
||||||
('Рыба', 'fish', 10, 1),
|
(NULL, 'Рыба', 'ryba', 10, 1),
|
||||||
('Готовая продукция', 'gotovaya-produkciya', 20, 1),
|
(NULL, 'Морепродукты', 'moreprodukty', 20, 1),
|
||||||
('Полуфабрикаты', 'polufabrikaty', 30, 1),
|
(NULL, 'Фрукты, овощи, ягоды, грибы', 'frukty-ovoschi-yagody-griby', 30, 1),
|
||||||
('Мясо', 'myaso', 40, 1),
|
(NULL, 'Икра', 'ikra', 40, 1),
|
||||||
('Сыры', 'syry', 50, 1),
|
(NULL, 'Бакалея', 'bakaleya', 50, 1),
|
||||||
('Ягоды', 'yagody', 60, 1),
|
(NULL, 'Полуфабрикаты', 'polufabrikaty', 60, 1),
|
||||||
('Овощи', 'ovoschi', 70, 1);
|
(NULL, 'Мясная гастрономия', 'myasnaya-gastronomiya', 70, 1),
|
||||||
|
(NULL, 'Мясо', 'myaso', 80, 1),
|
||||||
|
(NULL, 'Сыры', 'syry', 90, 1),
|
||||||
|
(NULL, 'Молочная продукция', 'molochnaya-produkciya', 100, 1);
|
||||||
|
|
||||||
|
INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'ryba'), 'Красная рыба', 'krasnaya-ryba', 10, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'ryba'), 'Белая рыба', 'belaya-ryba', 20, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'ryba'), 'Охлажденная и живая рыба', 'ohlazhdennaya-i-zhivaya-ryba', 30, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'ryba'), 'Рыбное филе', 'rybnoe-file', 40, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'ryba'), 'Фарш рыбный', 'farsh-rybnyj', 50, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'ryba'), 'Вяленая рыба', 'vyalenaya-ryba', 60, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'ryba'), 'Малосольная рыба', 'malosolnaya-ryba', 70, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'ryba'), 'Жареная рыба', 'zharenaya-ryba', 80, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'ryba'), 'Копченая рыба', 'kopchenaya-ryba', 90, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'ryba'), 'Пресервы и консервы', 'preservy-i-konservy', 100, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'ryba'), 'Рыбные стейки', 'rybnye-stejki', 110, 1);
|
||||||
|
|
||||||
|
INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'krasnaya-ryba'), 'Брюшки лосося', 'bryushki-lososya', 10, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'krasnaya-ryba'), 'Горбуша', 'gorbushka', 20, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'krasnaya-ryba'), 'Голец', 'golec', 30, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'krasnaya-ryba'), 'Кета', 'keta', 40, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'krasnaya-ryba'), 'Кижуч', 'kizhuch', 50, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'krasnaya-ryba'), 'Лосось', 'losos', 60, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'krasnaya-ryba'), 'Молоки лососевые', 'moloki-lososevye', 70, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'krasnaya-ryba'), 'Нерка', 'nerka', 80, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'krasnaya-ryba'), 'Семга', 'semga', 90, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'krasnaya-ryba'), 'Форель', 'forel', 100, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'krasnaya-ryba'), 'Чавыча', 'chavycha', 110, 1);
|
||||||
|
|
||||||
|
INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Барабулька', 'barabulka', 10, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Белый Амур', 'belyj-amur', 20, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Вобла', 'vobla', 30, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Вомер', 'vomer', 40, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Густера', 'gustera', 50, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Дори', 'dori', 60, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Дорадо', 'dorado', 70, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Жерех', 'zhereh', 80, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Зубатка', 'zubatka', 90, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Игла рыба', 'igla-ryba', 100, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Камбала', 'kambala', 110, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Карп', 'karp', 120, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Карась', 'karas', 130, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Кефаль', 'kefal', 140, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Килька', 'kilka', 150, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Конгрио', 'kongrio', 160, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Корюшка', 'koryushka', 170, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Лакедра', 'lakedra', 180, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Лещ', 'lesch', 190, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Ледяная', 'ledyanaya', 200, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Лемонема', 'lemonema', 210, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Марлин', 'marlin', 220, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Макрурус', 'makrurus', 230, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Масляная', 'maslyanaya', 240, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Минтай', 'mintaj', 250, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Мойва', 'mojva', 260, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Молочная рыба Ханос', 'molochnaya-ryba-hanos', 270, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Навага', 'navaga', 280, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Омуль', 'omul', 290, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Осетр', 'osetr', 300, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Окунь', 'okun', 310, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Палтус', 'paltus', 320, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Пангасиус', 'pangasius', 330, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Пелядь', 'pelyad', 340, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Плотва', 'plotva', 350, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Пикша', 'piksha', 360, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Путассу', 'putassu', 370, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Рыбец', 'rybec', 380, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Сайда', 'sajda', 390, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Сазан', 'sazan', 400, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Салака', 'salaka', 410, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Сардина Иваси', 'sardina-ivasi', 420, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Сельдь', 'seld', 430, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Сибас', 'sibas', 440, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Сиг', 'sig', 450, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Синец', 'sinec', 460, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Скумбрия', 'skumbriya', 470, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Ставрида', 'stavrida', 480, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Сом', 'som', 490, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Судак', 'sudak', 500, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Тарань', 'taran', 510, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Терпуг', 'terpug', 520, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Тилапия', 'tilapiya', 530, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Толстолобик', 'tolstolobik', 540, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Треска', 'treska', 550, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Тунец', 'tunec', 560, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Тюлька', 'tyulka', 570, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Угольная треска', 'ugolnaya-treska', 580, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Хамса', 'hamsa', 590, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Хоки', 'hoki', 600, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Хек', 'hek', 610, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Чехонь', 'chehon', 620, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'belaya-ryba'), 'Щука', 'schuka', 630, 1);
|
||||||
|
|
||||||
|
INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе горбуши', 'file-gorbushi', 10, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе кефали', 'file-kefali', 20, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе кижуча', 'file-kizhucha', 30, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе кеты', 'file-kety', 40, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе масляной', 'file-maslyanoj', 50, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе минтая', 'file-mintaya', 60, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе окуня', 'file-okunya', 70, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе палтуса', 'file-paltusa', 80, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе пангасиуса', 'file-pangasiusa', 90, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе пикши', 'file-pikshi', 100, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе сазана', 'file-sazana', 110, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе сельди', 'file-seldi', 120, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе сайды', 'file-sajdy', 130, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе семги', 'file-semgi', 140, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе судака', 'file-sudaka', 150, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе толстолобика', 'file-tolstolobika', 160, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе тилапии', 'file-tilapii', 170, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе трески', 'file-treski', 180, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе форели', 'file-foreli', 190, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе хека', 'file-heka', 200, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'rybnoe-file'), 'Филе щуки', 'file-schuki', 210, 1);
|
||||||
|
|
||||||
|
INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'kopchenaya-ryba'), 'Горячее копчение', 'goryachee-kopchenie', 10, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'kopchenaya-ryba'), 'Холодное копчение', 'holodnoe-kopchenie', 20, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'kopchenaya-ryba'), 'Копченые морепродукты', 'kopchenye-moreprodukty', 30, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'kopchenaya-ryba'), 'Балыки', 'balyki', 40, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'kopchenaya-ryba'), 'Брюшки и теша', 'bryushki-i-tesha', 50, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'kopchenaya-ryba'), 'Копченое филе', 'kopchenoe-file', 60, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'kopchenaya-ryba'), 'Рулеты', 'rulety', 70, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'kopchenaya-ryba'), 'Шашлычки', 'shashlychki', 80, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'kopchenaya-ryba'), 'Хребты', 'hrebty', 90, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'kopchenaya-ryba'), 'Юкола', 'yukola', 100, 1);
|
||||||
|
|
||||||
|
INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Краб', 'krab', 10, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Кальмар', 'kalmar', 20, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Вонголе', 'vongole', 30, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Креветки', 'krevetki', 40, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Мидии', 'midii', 50, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Морской гребешок', 'morskoj-grebeshok', 60, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Осьминог', 'osminog', 70, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Морской коктейль', 'morskoj-koktejl', 80, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Чука', 'chuka', 90, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Угорь', 'ugor', 100, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Для суши', 'dlya-sushi', 110, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Раки', 'raki', 120, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'moreprodukty'), 'Другое', 'drugoe-moreprodukty', 130, 1);
|
||||||
|
|
||||||
|
INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'krevetki'), 'Королевские и ванамей', 'korolevskie-i-vanamej', 10, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'krevetki'), 'Северные', 'severnye-krevetki', 20, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'krevetki'), 'Тигровые', 'tigrovye-krevetki', 30, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'krevetki'), 'Очищенные', 'ochischennye-krevetki', 40, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'krevetki'), 'В панировке', 'krevetki-v-panirovke', 50, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'krevetki'), 'Лангустины', 'langustiny', 60, 1);
|
||||||
|
|
||||||
|
INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'frukty-ovoschi-yagody-griby'), 'Фрукты', 'frukty', 10, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'frukty-ovoschi-yagody-griby'), 'Овощи', 'ovoschi', 20, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'frukty-ovoschi-yagody-griby'), 'Ягоды', 'yagody', 30, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'frukty-ovoschi-yagody-griby'), 'Грибы', 'griby', 40, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'frukty-ovoschi-yagody-griby'), 'Овощные и ягодные смеси', 'ovoschnye-i-yagodnye-smesi', 50, 1);
|
||||||
|
|
||||||
|
INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'ikra'), 'Красная икра', 'krasnaya-ikra', 10, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'ikra'), 'Икра сельди', 'ikra-seldi', 20, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'ikra'), 'Икра минтая', 'ikra-mintaya', 30, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'ikra'), 'Икра палтуса', 'ikra-paltusa', 40, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'ikra'), 'Икра трески', 'ikra-treski', 50, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'ikra'), 'Икра щуки', 'ikra-schuki', 60, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'ikra'), 'Икра Масаго и Тобико', 'ikra-masago-i-tobiko', 70, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'ikra'), 'Икра для самостоятельной засолки', 'ikra-dlya-samostoyatelnoj-zasolki', 80, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'ikra'), 'Вяленая икра', 'vyalenaya-ikra', 90, 1);
|
||||||
|
|
||||||
|
INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'bakaleya'), 'Снеки и орехи', 'sneki-i-orehi', 10, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'bakaleya'), 'Овощная консервация', 'ovoschnaya-konservaciya', 20, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'bakaleya'), 'Оливковое масло', 'olivkovoe-maslo', 30, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'bakaleya'), 'Соусы и уксусы', 'sousy-i-uksusy', 40, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'bakaleya'), 'Макаронные изделия', 'makaronnye-izdeliya', 50, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'bakaleya'), 'Приправы и Специи', 'pripravy-i-specii', 60, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'bakaleya'), 'Крупы и мука', 'krupy-i-muka', 70, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'bakaleya'), 'Соль и сахар', 'sol-i-sahar', 80, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'bakaleya'), 'Масло растительное', 'maslo-rastitelnoe', 90, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'bakaleya'), 'Кофе', 'kofe', 100, 1);
|
||||||
|
|
||||||
|
INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Рыбные котлеты', 'rybnye-kotlety', 10, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Крабовые палочки', 'krabovye-palochki', 20, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Кальмары и креветки в панировке', 'kalmary-i-krevetki-v-panirovke', 30, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Наггетсы и стрипсы', 'naggetsy-i-stripsy', 40, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Сырные и рыбные палочки', 'syrnye-i-rybnye-palochki', 50, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Чебуреки', 'chebureki', 60, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Мясные котлеты', 'myasnye-kotlety', 70, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Супы', 'supy', 80, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Фаршированная продукция', 'farshirovannaya-produkciya', 90, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Полуфабрикаты из мяса и птицы', 'polufabrikaty-iz-myasa-i-pticy', 100, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Блинчики и оладьи', 'blinchiki-i-oladi', 110, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Овощные полуфабрикаты', 'ovoschnye-polufabrikaty', 120, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Сырники', 'syrniki', 130, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Пельмени и вареники', 'pelmeni-i-vareniki', 140, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Другие полуфабрикаты', 'drugie-polufabrikaty', 150, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Жареная рыба и котлеты', 'zharenaya-ryba-i-kotlety', 160, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'polufabrikaty'), 'Колбаски и купаты', 'kolbaski-i-kupaty', 170, 1);
|
||||||
|
|
||||||
|
INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myasnaya-gastronomiya'), 'Из мяса птицы', 'iz-myasa-pticy', 10, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myasnaya-gastronomiya'), 'Из мяса свинины', 'iz-myasa-svininy', 20, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myasnaya-gastronomiya'), 'Из мяса говядины', 'iz-myasa-govyadiny', 30, 1);
|
||||||
|
|
||||||
|
INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Говядина', 'govyadina', 10, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Мраморная говядина', 'mramornaya-govyadina', 20, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Свинина', 'svinina', 30, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Баранина', 'baranina', 40, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Буйвол', 'bujvol', 50, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Курица', 'kurica', 60, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Кролик', 'krolik', 70, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Индейка', 'indejka', 80, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Телятина', 'telyatina', 90, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Утка', 'utka', 100, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Гусь', 'gus', 110, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Цыплята Корнишоны', 'cyplyata-kornishony', 120, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Перепела', 'perepela', 130, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Субпродукты', 'subprodukty', 140, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Мясные полуфабрикаты', 'myasnye-polufabrikaty', 150, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Туши и полутуши', 'tushi-i-polutushi', 160, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Котлетное мясо', 'kotletnoe-myaso', 170, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Суповые наборы', 'supovye-nabory', 180, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Мясо для стейков', 'myaso-dlya-stejkov', 190, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Филе птицы', 'file-pticy', 200, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Мякоть говядины', 'myakot-govyadiny', 210, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Толстый и тонкий край', 'tolstyj-i-tonkij-kraj', 220, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Мясо на кости', 'myaso-na-kosti', 230, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Вырезка', 'vyrezka', 240, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Языки', 'yazyki', 250, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Фарш', 'farsh', 260, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'myaso'), 'Охлажденное мясо', 'ohlazhdennoe-myaso', 270, 1);
|
||||||
|
|
||||||
|
INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'syry'), 'Твердые', 'tverdye-syry', 10, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'syry'), 'Полутвердые', 'polutverdye-syry', 20, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'syry'), 'Рассольные сыры', 'rassolnye-syry', 30, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'syry'), 'С плесенью', 'syry-s-plesenyu', 40, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'syry'), 'Со вкусами', 'syry-so-vkusami', 50, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'syry'), 'Элитные сыры', 'elitnye-syry', 60, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'syry'), 'Мягкие сыры', 'myagkie-syry', 70, 1);
|
||||||
|
|
||||||
|
INSERT INTO categories (parent_id, name, slug, sort_order, is_active) VALUES
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'molochnaya-produkciya'), 'Молоко', 'moloko', 10, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'molochnaya-produkciya'), 'Сливки', 'slivki', 20, 1),
|
||||||
|
((SELECT id FROM categories c WHERE c.slug = 'molochnaya-produkciya'), 'Сливочное масло', 'slivochnoe-maslo', 30, 1);
|
||||||
|
|
||||||
INSERT INTO promocodes
|
INSERT INTO promocodes
|
||||||
(code, title, discount_type, discount_value, min_order_amount, applies_to, is_active, is_featured)
|
(code, title, discount_type, discount_value, min_order_amount, applies_to, is_active, is_featured, show_in_price_tiers, price_tier_label)
|
||||||
VALUES
|
VALUES
|
||||||
('Рыбсток7000', '300 руб. при заказе от 7000 руб.', 'fixed', 300.00, 7000.00, 'order', 1, 1),
|
('Рыбсток7000', '300 руб. при заказе от 7000 руб.', 'fixed', 300.00, 7000.00, 'order', 1, 1, 0, NULL),
|
||||||
('Рыбсток20000', '5% при заказе от 20000 руб.', 'percent', 5.00, 20000.00, 'order', 1, 1),
|
('Рыбсток20000', '5% при заказе от 20000 руб.', 'percent', 5.00, 20000.00, 'order', 1, 1, 1, 'Цена от 20 000 руб. наличными'),
|
||||||
('Рыбсток50000', '7% при заказе от 50000 руб.', 'percent', 7.00, 50000.00, 'order', 1, 1);
|
('Рыбсток50000', '7% при заказе от 50000 руб.', 'percent', 7.00, 50000.00, 'order', 1, 1, 1, 'Цена от 50 000 руб. наличными');
|
||||||
|
|
||||||
INSERT INTO site_settings (setting_key, setting_value, value_type, group_name) VALUES
|
INSERT INTO site_settings (setting_key, setting_value, value_type, group_name) VALUES
|
||||||
('site_phone', '+7-977-736-33-63', 'string', 'contacts'),
|
('site_phone', '+7-977-736-33-63', 'string', 'contacts'),
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# План переноса товаров со старого сайта
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
Перенести товары со старого сайта в новую базу без ручного копирования и без потери структуры, SEO и индексации.
|
||||||
|
|
||||||
|
## Что нужно перенести
|
||||||
|
|
||||||
|
Для каждого товара:
|
||||||
|
|
||||||
|
- название;
|
||||||
|
- старая ссылка;
|
||||||
|
- новый slug;
|
||||||
|
- категория и подкатегория;
|
||||||
|
- описание;
|
||||||
|
- краткое описание;
|
||||||
|
- информация о товаре;
|
||||||
|
- `title`;
|
||||||
|
- `description`;
|
||||||
|
- `keywords`;
|
||||||
|
- H1, если он отличается от названия;
|
||||||
|
- фото;
|
||||||
|
- фасовки;
|
||||||
|
- цены по фасовкам;
|
||||||
|
- единицы измерения;
|
||||||
|
- доступность/публикация.
|
||||||
|
|
||||||
|
Для каждой категории:
|
||||||
|
|
||||||
|
- название;
|
||||||
|
- старая ссылка;
|
||||||
|
- новый slug;
|
||||||
|
- H1;
|
||||||
|
- `title`;
|
||||||
|
- `description`;
|
||||||
|
- `keywords`;
|
||||||
|
- текст категории;
|
||||||
|
- родительская категория;
|
||||||
|
- порядок в меню.
|
||||||
|
|
||||||
|
## Возможные способы переноса
|
||||||
|
|
||||||
|
### 1. Выгрузка CSV/Excel
|
||||||
|
|
||||||
|
Лучший вариант, если старый сайт умеет выгружать товары.
|
||||||
|
|
||||||
|
Нужны отдельные колонки для категорий, фасовок, цен, SEO и старых ссылок.
|
||||||
|
|
||||||
|
### 2. Доступ к базе старого сайта
|
||||||
|
|
||||||
|
Самый точный вариант, если можно получить дамп базы или доступ к phpMyAdmin.
|
||||||
|
|
||||||
|
Можно написать миграционный PHP-скрипт, который перенесет категории, товары, фото, SEO и ссылки.
|
||||||
|
|
||||||
|
### 3. Парсинг старого сайта
|
||||||
|
|
||||||
|
Рабочий вариант, если нет выгрузки и доступа к базе.
|
||||||
|
|
||||||
|
Скрипт обходит старые URL, собирает HTML, title, description, keywords, хлебные крошки, описание, фото и цены.
|
||||||
|
|
||||||
|
Минус: зависит от верстки старого сайта, поэтому требует отдельного тестирования.
|
||||||
|
|
||||||
|
### 4. Ручной Excel-шаблон
|
||||||
|
|
||||||
|
Запасной вариант, если автоматический перенос невозможен.
|
||||||
|
|
||||||
|
Мы готовим шаблон, заполняем его товарами, затем импортируем в новую базу.
|
||||||
|
|
||||||
|
## Что уже заложено в новой базе
|
||||||
|
|
||||||
|
- `categories.legacy_path` - старый URL категории;
|
||||||
|
- `products.legacy_path` - старый URL товара;
|
||||||
|
- `categories.seo_title`;
|
||||||
|
- `categories.seo_description`;
|
||||||
|
- `categories.seo_keywords`;
|
||||||
|
- `categories.h1`;
|
||||||
|
- `products.seo_title`;
|
||||||
|
- `products.seo_description`;
|
||||||
|
- `products.seo_keywords`;
|
||||||
|
- `products.h1`;
|
||||||
|
- таблица `redirects` для 301-редиректов;
|
||||||
|
- таблица `product_images`;
|
||||||
|
- таблица `product_variants`.
|
||||||
|
|
||||||
|
## Отдельный этап работ
|
||||||
|
|
||||||
|
Перенос товаров выделяется в отдельную задачу после базовой структуры каталога и страниц.
|
||||||
|
|
||||||
|
Порядок:
|
||||||
|
|
||||||
|
1. Получить источник данных старого сайта: выгрузку, базу или список URL.
|
||||||
|
2. Сопоставить старые категории с новой структурой.
|
||||||
|
3. Подготовить импортёр.
|
||||||
|
4. Сделать тестовый импорт 10-20 товаров.
|
||||||
|
5. Проверить категории, фото, цены, фасовки, SEO и старые ссылки.
|
||||||
|
6. Запустить полный импорт.
|
||||||
|
7. Сформировать 301-редиректы.
|
||||||
|
8. Проверить выборочно страницы товаров и категорий.
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
- популярные товары;
|
- популярные товары;
|
||||||
- настройки сайта;
|
- настройки сайта;
|
||||||
- праздничные/нерабочие дни.
|
- праздничные/нерабочие дни.
|
||||||
|
- старые URL и 301-редиректы для сохранения индексации.
|
||||||
|
|
||||||
## Главная логика связей
|
## Главная логика связей
|
||||||
|
|
||||||
@@ -45,6 +46,10 @@
|
|||||||
- меню каталога;
|
- меню каталога;
|
||||||
- SEO-текст для страниц категорий.
|
- SEO-текст для страниц категорий.
|
||||||
|
|
||||||
|
Для переноса со старого сайта у категорий есть поле `legacy_path`. Если старый URL отличается от нового slug, старый путь нужно сохранить и добавить 301-редирект в таблицу `redirects`.
|
||||||
|
|
||||||
|
Для SEO у категорий есть поля `h1`, `seo_title`, `seo_description`, `seo_keywords`.
|
||||||
|
|
||||||
### Товары
|
### Товары
|
||||||
|
|
||||||
Таблица `products` хранит основную карточку товара:
|
Таблица `products` хранит основную карточку товара:
|
||||||
@@ -60,6 +65,10 @@
|
|||||||
- доступность;
|
- доступность;
|
||||||
- признаки "новинка", "хит", "акция".
|
- признаки "новинка", "хит", "акция".
|
||||||
|
|
||||||
|
Для товаров также есть поле `legacy_path`, чтобы переносить старые ссылки без потери индексации.
|
||||||
|
|
||||||
|
Для SEO у товаров есть поля `h1`, `seo_title`, `seo_description`, `seo_keywords`.
|
||||||
|
|
||||||
### Фасовки
|
### Фасовки
|
||||||
|
|
||||||
Таблица `product_variants` хранит отдельные фасовки товара.
|
Таблица `product_variants` хранит отдельные фасовки товара.
|
||||||
@@ -106,6 +115,13 @@
|
|||||||
|
|
||||||
Если промокод действует на весь заказ, targets не нужны.
|
Если промокод действует на весь заказ, targets не нужны.
|
||||||
|
|
||||||
|
Для отображения выгодных цен в каталоге у промокодов есть поля:
|
||||||
|
|
||||||
|
- `show_in_price_tiers` - показывать как градацию цены;
|
||||||
|
- `price_tier_label` - подпись для карточки товара.
|
||||||
|
|
||||||
|
Так можно показывать покупателю базовую цену, цену от 20 000 руб. наличными и цену от 50 000 руб. наличными.
|
||||||
|
|
||||||
### Доставка
|
### Доставка
|
||||||
|
|
||||||
Таблица `delivery_calculations` хранит результат расчета:
|
Таблица `delivery_calculations` хранит результат расчета:
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Шаг 3. Первые PHP-классы и вывод данных
|
||||||
|
|
||||||
|
## Что сделано
|
||||||
|
|
||||||
|
Созданы первые классы для работы сайта на чистом PHP:
|
||||||
|
|
||||||
|
- `app/Controllers/HomeController.php`
|
||||||
|
- `app/Repositories/BaseRepository.php`
|
||||||
|
- `app/Repositories/CategoryRepository.php`
|
||||||
|
- `app/Repositories/ProductRepository.php`
|
||||||
|
- `app/Repositories/ProductVariantRepository.php`
|
||||||
|
- `app/Repositories/SettingRepository.php`
|
||||||
|
|
||||||
|
Главная страница больше не содержит бизнес-логику напрямую. Теперь `public/index.php` вызывает `HomeController`, а контроллер получает данные через репозитории.
|
||||||
|
|
||||||
|
Также в базовый layout добавлены:
|
||||||
|
|
||||||
|
- meta title;
|
||||||
|
- meta description;
|
||||||
|
- meta keywords;
|
||||||
|
- хлебные крошки;
|
||||||
|
- кнопка "Вернуться назад".
|
||||||
|
|
||||||
|
## Что выводится на главной
|
||||||
|
|
||||||
|
Главная страница теперь показывает:
|
||||||
|
|
||||||
|
- статус подключения к MariaDB;
|
||||||
|
- категории из таблицы `categories`, строго по дереву старого сайта;
|
||||||
|
- вложенность категорий до третьего уровня;
|
||||||
|
- каталог товаров прямо на главной странице;
|
||||||
|
- настройки доставки из таблицы `site_settings`;
|
||||||
|
- блок популярных товаров из опубликованных товаров и фасовок.
|
||||||
|
- градации выгодной цены: базовая цена, цена от 20 000 руб. наличными, цена от 50 000 руб. наличными.
|
||||||
|
|
||||||
|
Если таблицы еще не установлены, сайт не падает, а показывает сообщение с подсказкой.
|
||||||
|
|
||||||
|
## Что добавлено в админку
|
||||||
|
|
||||||
|
`admin/index.php` теперь показывает стартовую сетку разделов будущей панели:
|
||||||
|
|
||||||
|
- Товары;
|
||||||
|
- Категории;
|
||||||
|
- Фасовки;
|
||||||
|
- Заказы;
|
||||||
|
- Промокоды;
|
||||||
|
- Акции;
|
||||||
|
- Популярные товары;
|
||||||
|
- Отзывы;
|
||||||
|
- Настройки;
|
||||||
|
- Прайс-лист.
|
||||||
|
|
||||||
|
Это пока не полноценная админка, а основа навигации для следующих шагов.
|
||||||
|
|
||||||
|
## Что добавлено для цен
|
||||||
|
|
||||||
|
В `promocodes` добавлены поля:
|
||||||
|
|
||||||
|
- `show_in_price_tiers`;
|
||||||
|
- `price_tier_label`.
|
||||||
|
|
||||||
|
Создан `app/Services/PriceTierService.php`, который считает отображаемые цены для карточки товара:
|
||||||
|
|
||||||
|
- базовая цена;
|
||||||
|
- минус 5% при заказе от 20 000 руб. за наличный расчет;
|
||||||
|
- минус 7% при заказе от 50 000 руб. за наличный расчет.
|
||||||
|
|
||||||
|
Это не отменяет ввод промокода в корзине. Это только понятная подсказка в каталоге, чтобы человек заранее видел выгоду крупного заказа.
|
||||||
|
|
||||||
|
## Что нужно проверить
|
||||||
|
|
||||||
|
1. Если база еще не установлена, открыть `/install.php?token=rybstock-local-install`.
|
||||||
|
2. После установки открыть главную страницу.
|
||||||
|
3. На главной должны появиться стартовые категории из `database/seed.sql`.
|
||||||
|
4. Открыть `/admin/` и проверить стартовые разделы админки.
|
||||||
|
|
||||||
|
Стартовые категории:
|
||||||
|
|
||||||
|
- Рыба;
|
||||||
|
- Рыбное филе;
|
||||||
|
- Рыбные стейки;
|
||||||
|
- Охлажденная рыба;
|
||||||
|
- Малосольная рыба;
|
||||||
|
- Копченая рыба;
|
||||||
|
- Вяленая рыба;
|
||||||
|
- Морепродукты;
|
||||||
|
- Фрукты, овощи, ягоды, грибы;
|
||||||
|
- Икра;
|
||||||
|
- Бакалея;
|
||||||
|
- Полуфабрикаты;
|
||||||
|
- Мясная гастрономия;
|
||||||
|
- Мясо;
|
||||||
|
- Сыры;
|
||||||
|
- Молочная продукция.
|
||||||
|
|
||||||
|
Важно: названия категорий уже приведены к старому дереву. Для полного сохранения индексации еще нужны точные старые URL категорий и товаров. В схеме для этого предусмотрены `legacy_path` и таблица `redirects`.
|
||||||
|
|
||||||
|
## Следующий шаг
|
||||||
|
|
||||||
|
Шаг 4:
|
||||||
|
|
||||||
|
1. Сделать базовый роутинг для страниц сайта.
|
||||||
|
2. Создать страницу полного каталога.
|
||||||
|
3. Вывести категории и товары из базы.
|
||||||
|
4. Подготовить первые формы админки для категорий и товаров.
|
||||||
|
|
||||||
|
Перенос товаров вынесен в отдельный документ: `docs/ПЛАН_ПЕРЕНОСА_ТОВАРОВ.md`.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once dirname(__DIR__, 2) . '/admin/index.php';
|
||||||
@@ -10,6 +10,50 @@ body {
|
|||||||
background: #f7f8f4;
|
background: #f7f8f4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.site-utility {
|
||||||
|
width: min(1080px, calc(100% - 48px));
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: center;
|
||||||
|
margin: 18px auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
appearance: none;
|
||||||
|
border: 1px solid #cfd8c9;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1d5d3b;
|
||||||
|
background: #ffffff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:hover {
|
||||||
|
border-color: #9fb194;
|
||||||
|
background: #f5f8f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 7px;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #687466;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs a {
|
||||||
|
color: #1d5d3b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs-separator {
|
||||||
|
color: #a3ad9d;
|
||||||
|
}
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -25,6 +69,10 @@ body {
|
|||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.start-screen.wide {
|
||||||
|
width: min(1080px, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
.eyebrow {
|
.eyebrow {
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -42,6 +90,25 @@ p {
|
|||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #1d5d3b;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.status-box {
|
.status-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -78,3 +145,195 @@ p {
|
|||||||
.primary-button:hover {
|
.primary-button:hover {
|
||||||
background: #16492e;
|
background: #16492e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 18px;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #dfe5da;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fbfcf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.products-panel {
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading .eyebrow,
|
||||||
|
.section-heading h2 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtle-link {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #687466;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-list .category-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 8px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-list li {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-list.depth-1 li {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-list.depth-2 {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-list.depth-2 li {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-list div {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-list dt {
|
||||||
|
color: #687466;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-list dd {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-grid,
|
||||||
|
.admin-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card,
|
||||||
|
.admin-tile {
|
||||||
|
min-height: 96px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid #dfe5da;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card p {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #687466;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card .product-category {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #7a856f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-line {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #2d372f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-tiers {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 12px 0 0;
|
||||||
|
padding: 10px 0 0;
|
||||||
|
border-top: 1px solid #e3e8de;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-tiers li {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: baseline;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-tiers span {
|
||||||
|
color: #687466;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-tiers strong {
|
||||||
|
color: #1d5d3b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-tile {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
min-height: 72px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.site-utility {
|
||||||
|
width: calc(100% - 32px);
|
||||||
|
display: grid;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-list div {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Core\Database;
|
||||||
|
|
||||||
|
require_once dirname(__DIR__) . '/app/bootstrap.php';
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||||
|
|
||||||
|
echo "RybStock diagnostic\n";
|
||||||
|
echo "version: 2026-06-04-02\n";
|
||||||
|
echo "file: " . __FILE__ . "\n";
|
||||||
|
echo "base_path: " . base_path() . "\n";
|
||||||
|
echo "db: " . app_env('DB_DATABASE', '') . "\n";
|
||||||
|
echo "home_text_expected: Каталог открывается уже на главной странице\n";
|
||||||
|
echo "schema_mtime: " . date('Y-m-d H:i:s', filemtime(base_path('database/schema.sql'))) . "\n";
|
||||||
|
echo "seed_mtime: " . date('Y-m-d H:i:s', filemtime(base_path('database/seed.sql'))) . "\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = Database::pdo();
|
||||||
|
echo "db_status: connected\n";
|
||||||
|
|
||||||
|
$tables = $pdo->query('SHOW TABLES')->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
echo "tables_count: " . count($tables) . "\n";
|
||||||
|
|
||||||
|
if (in_array('categories', $tables, true)) {
|
||||||
|
echo "categories_count: " . $pdo->query('SELECT COUNT(*) FROM categories')->fetchColumn() . "\n";
|
||||||
|
echo "root_categories:\n";
|
||||||
|
|
||||||
|
$statement = $pdo->query('SELECT id, name, slug FROM categories WHERE parent_id IS NULL ORDER BY sort_order LIMIT 30');
|
||||||
|
foreach ($statement->fetchAll() as $category) {
|
||||||
|
echo "- {$category['name']} ({$category['slug']})\n";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "categories_table: missing\n";
|
||||||
|
}
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
echo "db_status: error\n";
|
||||||
|
echo "error: " . $exception->getMessage() . "\n";
|
||||||
|
}
|
||||||
+2
-16
@@ -2,22 +2,8 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use App\Core\Database;
|
use App\Controllers\HomeController;
|
||||||
|
|
||||||
require_once dirname(__DIR__) . '/app/bootstrap.php';
|
require_once dirname(__DIR__) . '/app/bootstrap.php';
|
||||||
|
|
||||||
$dbStatus = 'not_checked';
|
(new HomeController())->index();
|
||||||
|
|
||||||
try {
|
|
||||||
Database::pdo()->query('SELECT 1');
|
|
||||||
$dbStatus = 'connected';
|
|
||||||
} catch (Throwable $exception) {
|
|
||||||
$dbStatus = app_env('APP_DEBUG', 'false') === 'true'
|
|
||||||
? 'error: ' . $exception->getMessage()
|
|
||||||
: 'error';
|
|
||||||
}
|
|
||||||
|
|
||||||
view('pages/home', [
|
|
||||||
'title' => 'Рыбсток',
|
|
||||||
'dbStatus' => $dbStatus,
|
|
||||||
]);
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ $actualToken = $_GET['token'] ?? '';
|
|||||||
$canRun = $isLocal && $expectedToken !== '' && hash_equals($expectedToken, (string) $actualToken);
|
$canRun = $isLocal && $expectedToken !== '' && hash_equals($expectedToken, (string) $actualToken);
|
||||||
$result = null;
|
$result = null;
|
||||||
$error = null;
|
$error = null;
|
||||||
|
$stats = install_stats();
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
if (!$canRun) {
|
if (!$canRun) {
|
||||||
@@ -22,6 +23,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$pdo = Database::pdo();
|
$pdo = Database::pdo();
|
||||||
run_sql_file($pdo, base_path('database/schema.sql'));
|
run_sql_file($pdo, base_path('database/schema.sql'));
|
||||||
run_sql_file($pdo, base_path('database/seed.sql'));
|
run_sql_file($pdo, base_path('database/seed.sql'));
|
||||||
|
$stats = install_stats();
|
||||||
$result = 'База данных установлена. Таблицы и стартовые данные созданы.';
|
$result = 'База данных установлена. Таблицы и стартовые данные созданы.';
|
||||||
} catch (Throwable $exception) {
|
} catch (Throwable $exception) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
@@ -32,6 +34,33 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function install_stats(): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$pdo = Database::pdo();
|
||||||
|
$tables = $pdo->query('SHOW TABLES')->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
$categoryCount = in_array('categories', $tables, true)
|
||||||
|
? (int) $pdo->query('SELECT COUNT(*) FROM categories')->fetchColumn()
|
||||||
|
: 0;
|
||||||
|
$firstCategories = in_array('categories', $tables, true)
|
||||||
|
? $pdo->query('SELECT name FROM categories WHERE parent_id IS NULL ORDER BY sort_order LIMIT 12')->fetchAll(PDO::FETCH_COLUMN)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'database' => app_env('DB_DATABASE', ''),
|
||||||
|
'tables_count' => count($tables),
|
||||||
|
'category_count' => $categoryCount,
|
||||||
|
'first_categories' => $firstCategories,
|
||||||
|
];
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'error' => $exception->getMessage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function run_sql_file(PDO $pdo, string $path): void
|
function run_sql_file(PDO $pdo, string $path): void
|
||||||
{
|
{
|
||||||
if (!is_file($path)) {
|
if (!is_file($path)) {
|
||||||
@@ -62,6 +91,39 @@ function run_sql_file(PDO $pdo, string $path): void
|
|||||||
<h1>Установка базы</h1>
|
<h1>Установка базы</h1>
|
||||||
<p>Эта страница применяет `database/schema.sql` и `database/seed.sql`. Таблицы будут пересозданы.</p>
|
<p>Эта страница применяет `database/schema.sql` и `database/seed.sql`. Таблицы будут пересозданы.</p>
|
||||||
|
|
||||||
|
<div class="status-box">
|
||||||
|
<span>Версия install.php:</span>
|
||||||
|
<strong>2026-06-04-02</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-box">
|
||||||
|
<span>Файл:</span>
|
||||||
|
<strong><?= htmlspecialchars(__FILE__, ENT_QUOTES, 'UTF-8') ?></strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($stats['ok'] ?? false): ?>
|
||||||
|
<div class="status-box">
|
||||||
|
<span>База:</span>
|
||||||
|
<strong><?= htmlspecialchars((string) $stats['database'], ENT_QUOTES, 'UTF-8') ?></strong>
|
||||||
|
</div>
|
||||||
|
<div class="status-box">
|
||||||
|
<span>Таблиц:</span>
|
||||||
|
<strong><?= htmlspecialchars((string) $stats['tables_count'], ENT_QUOTES, 'UTF-8') ?></strong>
|
||||||
|
</div>
|
||||||
|
<div class="status-box">
|
||||||
|
<span>Категорий:</span>
|
||||||
|
<strong><?= htmlspecialchars((string) $stats['category_count'], ENT_QUOTES, 'UTF-8') ?></strong>
|
||||||
|
</div>
|
||||||
|
<?php if (!empty($stats['first_categories'])): ?>
|
||||||
|
<div class="status-box">
|
||||||
|
<span>Верхние категории:</span>
|
||||||
|
<strong><?= htmlspecialchars(implode(', ', $stats['first_categories']), ENT_QUOTES, 'UTF-8') ?></strong>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="status-box danger"><?= htmlspecialchars((string) ($stats['error'] ?? 'Нет статистики'), ENT_QUOTES, 'UTF-8') ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (!$isLocal): ?>
|
<?php if (!$isLocal): ?>
|
||||||
<div class="status-box danger">Установка доступна только при APP_ENV=local.</div>
|
<div class="status-box danger">Установка доступна только при APP_ENV=local.</div>
|
||||||
<?php elseif (!$canRun): ?>
|
<?php elseif (!$canRun): ?>
|
||||||
|
|||||||
+19
-1
@@ -4,16 +4,34 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
/** @var string $templatePath */
|
/** @var string $templatePath */
|
||||||
/** @var string $title */
|
/** @var string $title */
|
||||||
|
/** @var string $metaDescription */
|
||||||
|
/** @var string $metaKeywords */
|
||||||
|
/** @var array<int, array{title: string, url?: string}> $breadcrumbs */
|
||||||
|
/** @var bool $showBackButton */
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="ru">
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title><?= htmlspecialchars($title ?? 'Рыбсток', ENT_QUOTES, 'UTF-8') ?></title>
|
<title><?= e($title) ?></title>
|
||||||
|
<?php if ($metaDescription !== ''): ?>
|
||||||
|
<meta name="description" content="<?= e($metaDescription) ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($metaKeywords !== ''): ?>
|
||||||
|
<meta name="keywords" content="<?= e($metaKeywords) ?>">
|
||||||
|
<?php endif; ?>
|
||||||
<link rel="stylesheet" href="/assets/css/app.css">
|
<link rel="stylesheet" href="/assets/css/app.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<header class="site-utility">
|
||||||
|
<?php if ($showBackButton): ?>
|
||||||
|
<button class="back-button" type="button" onclick="history.length > 1 ? history.back() : location.href='/'">Вернуться назад</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php require base_path('views/partials/breadcrumbs.php'); ?>
|
||||||
|
</header>
|
||||||
|
|
||||||
<main class="page">
|
<main class="page">
|
||||||
<?php require $templatePath; ?>
|
<?php require $templatePath; ?>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
+121
-4
@@ -3,14 +3,131 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
/** @var string $dbStatus */
|
/** @var string $dbStatus */
|
||||||
|
/** @var array<int, array<string, mixed>> $categories */
|
||||||
|
/** @var array<int, array<string, mixed>> $catalogProducts */
|
||||||
|
/** @var array<int, array<string, mixed>> $popularProducts */
|
||||||
|
/** @var array<string, mixed> $settings */
|
||||||
|
/** @var string|null $notice */
|
||||||
|
/** @var string $buildVersion */
|
||||||
?>
|
?>
|
||||||
<section class="start-screen">
|
<section class="start-screen wide">
|
||||||
<p class="eyebrow">Старт проекта</p>
|
<p class="eyebrow">Шаг 3</p>
|
||||||
<h1>Рыбсток</h1>
|
<h1>Рыбсток</h1>
|
||||||
<p>Чистый PHP-проект подготовлен. Следующий шаг - схема базы данных и таблицы для каталога, фасовок, заказов и админки.</p>
|
<p>Каталог открывается уже на главной странице, чтобы покупатель сразу видел ассортимент и не делал лишних переходов.</p>
|
||||||
|
<p class="muted">Версия сборки: <?= e($buildVersion) ?></p>
|
||||||
|
|
||||||
<div class="status-box">
|
<div class="status-box">
|
||||||
<span>MariaDB:</span>
|
<span>MariaDB:</span>
|
||||||
<strong><?= htmlspecialchars($dbStatus, ENT_QUOTES, 'UTF-8') ?></strong>
|
<strong><?= e($dbStatus) ?></strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if ($notice !== null): ?>
|
||||||
|
<div class="status-box danger"><?= e($notice) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="content-grid">
|
||||||
|
<section class="panel">
|
||||||
|
<h2>Категории</h2>
|
||||||
|
<?php if ($categories === []): ?>
|
||||||
|
<p class="muted">Категории пока не найдены. Если схема уже применена, проверьте `database/seed.sql`.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php
|
||||||
|
$items = $categories;
|
||||||
|
$depth = 0;
|
||||||
|
require base_path('views/partials/category-tree.php');
|
||||||
|
?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>Настройки доставки</h2>
|
||||||
|
<dl class="settings-list">
|
||||||
|
<div>
|
||||||
|
<dt>Внутри МКАД бесплатно от</dt>
|
||||||
|
<dd><?= e((string) ($settings['inside_mkad_free_from'] ?? '5000')) ?> руб.</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Внутри МКАД платно</dt>
|
||||||
|
<dd><?= e((string) ($settings['inside_mkad_paid_price'] ?? '599')) ?> руб.</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>За МКАД расчет</dt>
|
||||||
|
<dd><?= e((string) ($settings['outside_mkad_base_price'] ?? '199')) ?> руб. + <?= e((string) ($settings['outside_mkad_km_price'] ?? '60')) ?> руб./км</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Подъем</dt>
|
||||||
|
<dd><?= e((string) ($settings['lift_price'] ?? '390')) ?> руб.</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="panel products-panel">
|
||||||
|
<div class="section-heading">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Каталог продукции</p>
|
||||||
|
<h2>Товары на главной</h2>
|
||||||
|
</div>
|
||||||
|
<a class="subtle-link" href="/catalog">Весь каталог</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($catalogProducts === []): ?>
|
||||||
|
<p class="muted">После наполнения базы товары будут отображаться здесь сразу на главной странице.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="product-grid">
|
||||||
|
<?php foreach ($catalogProducts as $product): ?>
|
||||||
|
<article class="product-card">
|
||||||
|
<p class="product-category"><?= e($product['category_name'] ?? 'Каталог') ?></p>
|
||||||
|
<h3><?= e($product['name']) ?></h3>
|
||||||
|
<p><?= e($product['variant_name'] ?? 'Выберите доступную фасовку') ?></p>
|
||||||
|
<?php if ($product['price_per_unit'] !== null): ?>
|
||||||
|
<span class="price-line">за <?= e($product['unit'] ?? 'ед.') ?>: <?= e((string) $product['price_per_unit']) ?> руб.</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($product['package_price'] !== null): ?>
|
||||||
|
<strong><?= e((string) $product['package_price']) ?> руб. за фасовку</strong>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($product['price_tiers'])): ?>
|
||||||
|
<ul class="price-tiers">
|
||||||
|
<?php foreach ($product['price_tiers'] as $tier): ?>
|
||||||
|
<li>
|
||||||
|
<span><?= e($tier['label']) ?></span>
|
||||||
|
<strong><?= e((string) $tier['price']) ?> руб.</strong>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<?php endif; ?>
|
||||||
|
</article>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel products-panel">
|
||||||
|
<h2>Популярные товары</h2>
|
||||||
|
<?php if ($popularProducts === []): ?>
|
||||||
|
<p class="muted">Товары появятся здесь после наполнения каталога и публикации фасовок.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="product-grid">
|
||||||
|
<?php foreach ($popularProducts as $product): ?>
|
||||||
|
<article class="product-card">
|
||||||
|
<h3><?= e($product['name']) ?></h3>
|
||||||
|
<p><?= e($product['variant_name'] ?? 'Фасовка не выбрана') ?></p>
|
||||||
|
<?php if ($product['package_price'] !== null): ?>
|
||||||
|
<strong><?= e((string) $product['package_price']) ?> руб.</strong>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($product['price_tiers'])): ?>
|
||||||
|
<ul class="price-tiers">
|
||||||
|
<?php foreach ($product['price_tiers'] as $tier): ?>
|
||||||
|
<li>
|
||||||
|
<span><?= e($tier['label']) ?></span>
|
||||||
|
<strong><?= e((string) $tier['price']) ?> руб.</strong>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<?php endif; ?>
|
||||||
|
</article>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/** @var array<int, array{title: string, url?: string}> $breadcrumbs */
|
||||||
|
?>
|
||||||
|
<?php if ($breadcrumbs !== []): ?>
|
||||||
|
<nav class="breadcrumbs" aria-label="Хлебные крошки">
|
||||||
|
<?php foreach ($breadcrumbs as $index => $item): ?>
|
||||||
|
<?php if ($index > 0): ?>
|
||||||
|
<span class="breadcrumbs-separator">/</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($item['url']) && $index < count($breadcrumbs) - 1): ?>
|
||||||
|
<a href="<?= e($item['url']) ?>"><?= e($item['title']) ?></a>
|
||||||
|
<?php else: ?>
|
||||||
|
<span><?= e($item['title']) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</nav>
|
||||||
|
<?php endif; ?>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/** @var array<int, array<string, mixed>> $items */
|
||||||
|
/** @var int $depth */
|
||||||
|
?>
|
||||||
|
<ul class="category-list depth-<?= e((string) $depth) ?>">
|
||||||
|
<?php foreach ($items as $category): ?>
|
||||||
|
<li>
|
||||||
|
<a href="/catalog/<?= e($category['slug']) ?>"><?= e($category['name']) ?></a>
|
||||||
|
<?php if (!empty($category['children'])): ?>
|
||||||
|
<?php
|
||||||
|
$previousItems = $items;
|
||||||
|
$items = $category['children'];
|
||||||
|
$depth++;
|
||||||
|
require __DIR__ . '/category-tree.php';
|
||||||
|
$depth--;
|
||||||
|
$items = $previousItems;
|
||||||
|
?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
@@ -41,17 +41,26 @@
|
|||||||
|
|
||||||
### 2.3. Категории
|
### 2.3. Категории
|
||||||
|
|
||||||
Основные категории для главного заголовка и меню:
|
Основные категории должны строго соответствовать старому сайту, чтобы сохранить дерево, ссылки, индексацию и целостность переноса:
|
||||||
|
|
||||||
- Рыба;
|
- Рыба;
|
||||||
- Готовая продукция;
|
- Рыбное филе;
|
||||||
|
- Рыбные стейки;
|
||||||
|
- Охлажденная рыба;
|
||||||
|
- Малосольная рыба;
|
||||||
|
- Копченая рыба;
|
||||||
|
- Вяленая рыба;
|
||||||
|
- Морепродукты;
|
||||||
|
- Фрукты, овощи, ягоды, грибы;
|
||||||
|
- Икра;
|
||||||
|
- Бакалея;
|
||||||
- Полуфабрикаты;
|
- Полуфабрикаты;
|
||||||
|
- Мясная гастрономия;
|
||||||
- Мясо;
|
- Мясо;
|
||||||
- Сыры;
|
- Сыры;
|
||||||
- Ягоды;
|
- Молочная продукция.
|
||||||
- Овощи.
|
|
||||||
|
|
||||||
Нужны категории и подкатегории. Подкатегории визуально отделить от категорий, например более мягким цветом, меньшим шрифтом или отступом.
|
Нужны категории и подкатегории. Подкатегории визуально отделить от категорий, например более мягким цветом, меньшим шрифтом или отступом. Для сохранения индексации нужны точные старые URL/slug каждой категории и товара; если они отличаются от текущих slug в seed-файле, их нужно заменить перед переносом.
|
||||||
|
|
||||||
## 3. Структура сайта
|
## 3. Структура сайта
|
||||||
|
|
||||||
@@ -64,6 +73,7 @@
|
|||||||
- Заметное сообщение: "Работаем без предоплаты. Оплата только при получении товара."
|
- Заметное сообщение: "Работаем без предоплаты. Оплата только при получении товара."
|
||||||
- Поиск по сайту.
|
- Поиск по сайту.
|
||||||
- Быстрый переход в полный каталог.
|
- Быстрый переход в полный каталог.
|
||||||
|
- Сам каталог должен быть уже открыт на главной странице, чтобы покупатель сразу видел товары и не делал лишних переходов.
|
||||||
- Понятная кнопка "Доставка и оплата".
|
- Понятная кнопка "Доставка и оплата".
|
||||||
- Верхнее меню с категориями и подкатегориями.
|
- Верхнее меню с категориями и подкатегориями.
|
||||||
|
|
||||||
@@ -629,4 +639,6 @@
|
|||||||
|
|
||||||
- Шаг 1 выполнен: выбрана техническая связка на чистом PHP, создан стартовый каркас проекта, подключение к MariaDB через `.env`, базовые папки и файл `ШАГ_1_ТЕХНИЧЕСКОЕ_РЕШЕНИЕ.md`.
|
- Шаг 1 выполнен: выбрана техническая связка на чистом PHP, создан стартовый каркас проекта, подключение к MariaDB через `.env`, базовые папки и файл `ШАГ_1_ТЕХНИЧЕСКОЕ_РЕШЕНИЕ.md`.
|
||||||
- Шаг 2 выполнен: создана SQL-схема базы `database/schema.sql`, стартовые данные `database/seed.sql`, страница установки `public/install.php` и документ `docs/ШАГ_2_БАЗА_ДАННЫХ.md`.
|
- Шаг 2 выполнен: создана SQL-схема базы `database/schema.sql`, стартовые данные `database/seed.sql`, страница установки `public/install.php` и документ `docs/ШАГ_2_БАЗА_ДАННЫХ.md`.
|
||||||
- Следующий шаг: применить схему через `/install.php?token=rybstock-local-install`, затем сделать первые PHP-классы для чтения категорий, товаров, фасовок и настроек.
|
- Шаг 3 выполнен: созданы первые PHP-классы, репозитории, контроллер главной страницы, вывод категорий/настроек/популярных товаров, каталог на главной, категории старого сайта в `database/seed.sql`, SEO-поля, хлебные крошки, кнопка назад, градации выгодных цен по промокодам и документ `docs/ШАГ_3_PHP_КЛАССЫ.md`.
|
||||||
|
- Перенос товаров выделен в отдельную задачу: `docs/ПЛАН_ПЕРЕНОСА_ТОВАРОВ.md`.
|
||||||
|
- Следующий шаг: сделать базовый роутинг, страницу полного каталога и первые формы админки для категорий/товаров.
|
||||||
|
|||||||
Reference in New Issue
Block a user