docs: update docs
This commit is contained in:
819
docs/BANNERS_WITH_FILTERS_SPECIFICATION.md
Normal file
819
docs/BANNERS_WITH_FILTERS_SPECIFICATION.md
Normal file
@@ -0,0 +1,819 @@
|
|||||||
|
# Техническое задание: Система баннеров с динамическими фильтрами товаров
|
||||||
|
|
||||||
|
## 1. Описание задачи
|
||||||
|
|
||||||
|
### 1.1. Проблема
|
||||||
|
|
||||||
|
Текущий слайдер на главной странице отображает только изображения, которые не являются кликабельными. Это делает его
|
||||||
|
бесполезным с точки зрения конверсии.
|
||||||
|
|
||||||
|
### 1.2. Бизнес-цель
|
||||||
|
|
||||||
|
Реализовать интерактивные баннеры, которые при клике будут:
|
||||||
|
|
||||||
|
- Открывать товары из определенной категории
|
||||||
|
- Показывать конкретный товар
|
||||||
|
- Отображать товары по заданному фильтру
|
||||||
|
- Позволять администраторам создавать и сохранять фильтры для повторного использования
|
||||||
|
|
||||||
|
### 1.3. Техническое решение
|
||||||
|
|
||||||
|
- Использовать существующую систему фильтрации на основе JSON-объектов
|
||||||
|
- Использовать стандартный функционал баннеров OpenCart
|
||||||
|
- Создать административный интерфейс для управления фильтрами
|
||||||
|
- Связать фильтры с баннерами через специальный формат URL в поле "ссылка" баннера
|
||||||
|
- Добавить отслеживание целей Яндекс.Метрики
|
||||||
|
|
||||||
|
**Важно:** Формат ссылки `#filter:{filter_name}` с префиксом якоря `#` обеспечивает обратную совместимость:
|
||||||
|
- На обычном сайте OpenCart ссылка не будет ломать навигацию (просто не ведет никуда)
|
||||||
|
- В SPA JavaScript перехватывает и обрабатывает такие ссылки специальным образом
|
||||||
|
|
||||||
|
## 2. Архитектура решения
|
||||||
|
|
||||||
|
### 2.1. Компоненты системы
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ ADMIN PANEL │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ 1. Конструктор фильтров (Filter Builder) │
|
||||||
|
│ - Создание JSON фильтров │
|
||||||
|
│ - Сохранение фильтров с именами │
|
||||||
|
│ - Редактирование и удаление фильтров │
|
||||||
|
│ │
|
||||||
|
│ 2. Управление баннерами (расширение OpenCart) │
|
||||||
|
│ - Привязка фильтра к слайду через ссылку │
|
||||||
|
│ - Формат ссылки: #filter:{filter_name} │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ BACKEND API │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ 1. FilterService │
|
||||||
|
│ - Сохранение/редактирование фильтров │
|
||||||
|
│ - Список сохраненных фильтров │
|
||||||
|
│ - Получение фильтра по имени │
|
||||||
|
│ │
|
||||||
|
│ 2. BannerHandler (расширение) │
|
||||||
|
│ - Добавление поля filter_name в ответ │
|
||||||
|
│ - Определение типа ссылки │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ FRONTEND SPA │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ 1. Banner.vue (обновление) │
|
||||||
|
│ - Обработка клика по слайду │
|
||||||
|
│ - Определение типа ссылки │
|
||||||
|
│ - Переход на соответствующую страницу │
|
||||||
|
│ │
|
||||||
|
│ 2. Router (обновление) │
|
||||||
|
│ - Новый роут: /products-filter │
|
||||||
|
│ - Параметры: filter_name или filter JSON │
|
||||||
|
│ │
|
||||||
|
│ 3. ProductsFilter.vue (новый компонент) │
|
||||||
|
│ - Загрузка товаров по фильтру │
|
||||||
|
│ - Интеграция с YaMetrika │
|
||||||
|
│ │
|
||||||
|
│ 4. YaMetrika Integration │
|
||||||
|
│ - Новая цель: BANNER_CLICK │
|
||||||
|
│ - Трекинг кликов по баннерам │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Детальная спецификация
|
||||||
|
|
||||||
|
### 3.1. Backend: Система управления фильтрами
|
||||||
|
|
||||||
|
#### 3.1.1. Таблица базы данных
|
||||||
|
|
||||||
|
**Создать таблицу:** `oc_telecart_filter`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE oc_telecart_filter
|
||||||
|
(
|
||||||
|
filter_id INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
filter_json TEXT NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (filter_id),
|
||||||
|
UNIQUE KEY unique_name (name)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.1.2. Model: FilterModel
|
||||||
|
|
||||||
|
**Путь:** `module/oc_telegram_shop/upload/oc_telegram_shop/src/Models/FilterModel.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
class FilterModel
|
||||||
|
{
|
||||||
|
public function create(array $data): int
|
||||||
|
{
|
||||||
|
// Создание нового фильтра
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(int $filterId, array $data): bool
|
||||||
|
{
|
||||||
|
// Обновление фильтра
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(int $filterId): bool
|
||||||
|
{
|
||||||
|
// Удаление фильтра
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getById(int $filterId): ?array
|
||||||
|
{
|
||||||
|
// Получение фильтра по ID
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getByName(string $name): ?array
|
||||||
|
{
|
||||||
|
// Получение фильтра по имени
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAll(): array
|
||||||
|
{
|
||||||
|
// Получение списка всех фильтров
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.1.3. Handler: FilterHandler
|
||||||
|
|
||||||
|
**Путь:** `module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/FilterHandler.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Handlers;
|
||||||
|
|
||||||
|
use App\Models\FilterModel;
|
||||||
|
use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||||
|
use Openguru\OpenCartFramework\Http\Request;
|
||||||
|
|
||||||
|
class FilterHandler
|
||||||
|
{
|
||||||
|
public function list(): JsonResponse
|
||||||
|
{
|
||||||
|
// GET /api/filter/list
|
||||||
|
// Возвращает список всех сохраненных фильтров
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
// GET /api/filter/show?name={filter_name}
|
||||||
|
// Возвращает фильтр по имени
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
// POST /api/filter/store
|
||||||
|
// Создание нового фильтра
|
||||||
|
// Body: { name: string, filter_json: object }
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
// POST /api/filter/update
|
||||||
|
// Обновление существующего фильтра
|
||||||
|
// Body: { id: int, name: string, filter_json: object }
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
// POST /api/filter/delete
|
||||||
|
// Удаление фильтра
|
||||||
|
// Body: { id: int }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.1.4. Обновление: BannerHandler
|
||||||
|
|
||||||
|
**Путь:** `module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/BannerHandler.php`
|
||||||
|
|
||||||
|
Обновить метод `show()` для поддержки фильтров:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function show(): JsonResponse
|
||||||
|
{
|
||||||
|
$data = [];
|
||||||
|
$bannerId = $this->settings->get('home_banner_id');
|
||||||
|
|
||||||
|
if ($bannerId) {
|
||||||
|
$banner = $this->registry->model_design_banner->getBanner($bannerId);
|
||||||
|
foreach ($banner as $index => $result) {
|
||||||
|
if (is_file(DIR_IMAGE . $result['image'])) {
|
||||||
|
$link = $result['link'];
|
||||||
|
|
||||||
|
// Определяем тип ссылки
|
||||||
|
$linkType = 'external';
|
||||||
|
$filterName = null;
|
||||||
|
|
||||||
|
if (preg_match('/^#filter:(.+)$/', $link, $matches)) {
|
||||||
|
$linkType = 'filter';
|
||||||
|
$filterName = $matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
$data[] = [
|
||||||
|
'id' => $index,
|
||||||
|
'title' => $result['title'],
|
||||||
|
'link' => $link,
|
||||||
|
'link_type' => $linkType,
|
||||||
|
'filter_name' => $filterName,
|
||||||
|
'image' => $this->imageTool->resize($result['image'], null, 200),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'data' => $data,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2. Frontend: Компоненты SPA
|
||||||
|
|
||||||
|
#### 3.2.1. Обновление: Banner.vue
|
||||||
|
|
||||||
|
**Путь:** `spa/src/components/Banner.vue`
|
||||||
|
|
||||||
|
Добавить обработку клика и определение типа ссылки:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="slides.length > 0" class="app-banner px-4">
|
||||||
|
<Swiper ...>
|
||||||
|
<SwiperSlide
|
||||||
|
v-for="slide in slides"
|
||||||
|
:key="slide.id"
|
||||||
|
@click="onSlideClick(slide)"
|
||||||
|
>
|
||||||
|
<img :src="slide.image" :alt="slide.title" class="cursor-pointer">
|
||||||
|
</SwiperSlide>
|
||||||
|
</Swiper>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {useRouter} from 'vue-router';
|
||||||
|
import {useYaMetrikaStore} from '@/stores/yaMetrikaStore';
|
||||||
|
import {YA_METRIKA_GOAL} from '@/constants/yaMetrikaGoals';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const yaMetrika = useYaMetrikaStore();
|
||||||
|
|
||||||
|
function onSlideClick(slide) {
|
||||||
|
yaMetrika.reachGoal(YA_METRIKA_GOAL.BANNER_CLICK, {
|
||||||
|
banner_title: slide.title,
|
||||||
|
link_type: slide.link_type
|
||||||
|
});
|
||||||
|
|
||||||
|
if (slide.link_type === 'filter') {
|
||||||
|
// Обработка ссылок формата #filter:{filter_name}
|
||||||
|
// Обычные якорные ссылки не ломают навигацию в OpenCart
|
||||||
|
router.push({
|
||||||
|
name: 'products-filter',
|
||||||
|
params: {filter_name: slide.filter_name}
|
||||||
|
});
|
||||||
|
} else if (slide.link_type === 'external') {
|
||||||
|
// Обработка внешних ссылок (если нужно)
|
||||||
|
console.log('External link:', slide.link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2.2. Новый компонент: ProductsFilter.vue
|
||||||
|
|
||||||
|
**Путь:** `spa/src/views/ProductsFilter.vue`
|
||||||
|
|
||||||
|
```vue
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="pb-20">
|
||||||
|
<ProductsList
|
||||||
|
:products="products"
|
||||||
|
:hasMore="hasMore"
|
||||||
|
:isLoading="isLoading"
|
||||||
|
:isLoadingMore="isLoadingMore"
|
||||||
|
@loadMore="onLoadMore"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {ref, onMounted} from 'vue';
|
||||||
|
import {useRoute} from 'vue-router';
|
||||||
|
import {useYaMetrikaStore} from '@/stores/yaMetrikaStore';
|
||||||
|
import ftch from '@/utils/ftch';
|
||||||
|
import ProductsList from '@/components/ProductsList.vue';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const yaMetrika = useYaMetrikaStore();
|
||||||
|
const products = ref([]);
|
||||||
|
const hasMore = ref(false);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const isLoadingMore = ref(false);
|
||||||
|
const page = ref(1);
|
||||||
|
const perPage = 20;
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
yaMetrika.pushHit(route.path, {title: 'Отфильтрованные товары'});
|
||||||
|
|
||||||
|
await fetchProducts();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchProducts() {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
const filterName = route.params.filter_name;
|
||||||
|
|
||||||
|
// Получаем фильтр по имени
|
||||||
|
const filterResponse = await ftch('filterShow', {name: filterName});
|
||||||
|
const filters = filterResponse.data.filter_json;
|
||||||
|
|
||||||
|
// Загружаем товары с фильтром
|
||||||
|
const response = await ftch('products', null, {
|
||||||
|
page: page.value,
|
||||||
|
perPage,
|
||||||
|
filters: filters,
|
||||||
|
});
|
||||||
|
|
||||||
|
products.value = response.data;
|
||||||
|
hasMore.value = response.meta.hasMore;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onLoadMore() {
|
||||||
|
// Реализация загрузки следующих страниц
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2.3. Обновление: Router
|
||||||
|
|
||||||
|
**Путь:** `spa/src/router.js`
|
||||||
|
|
||||||
|
Добавить новый роут:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
path: '/products-filter/:filter_name',
|
||||||
|
name
|
||||||
|
:
|
||||||
|
'products-filter',
|
||||||
|
component
|
||||||
|
:
|
||||||
|
() => import('@/views/ProductsFilter.vue')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2.4. Обновление: ftch.js
|
||||||
|
|
||||||
|
**Путь:** `spa/src/utils/ftch.js`
|
||||||
|
|
||||||
|
Добавить новые методы:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export async function fetchFilterByName(name) {
|
||||||
|
return await ftch('filterShow', {name});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchFilterList() {
|
||||||
|
return await ftch('filterList');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveFilter(data) {
|
||||||
|
return await apiFetch(`${BASE_URL}index.php?route=extension/tgshop/handle&api_action=filterStore`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2.5. Обновление: yaMetrikaGoals.js
|
||||||
|
|
||||||
|
**Путь:** `spa/src/constants/yaMetrikaGoals.js`
|
||||||
|
|
||||||
|
Добавить новую цель:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export const YA_METRIKA_GOAL = {
|
||||||
|
// ... существующие цели
|
||||||
|
BANNER_CLICK: 'banner_click',
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3. Admin Panel: Управление фильтрами
|
||||||
|
|
||||||
|
#### 3.3.1. Новый контроллер: FilterController
|
||||||
|
|
||||||
|
**Путь:** `module/oc_telegram_shop/upload/admin/controller/extension/tgshop/filter.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
class FilterController extends Controller
|
||||||
|
{
|
||||||
|
public function index(): void
|
||||||
|
{
|
||||||
|
// Список фильтров
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(): void
|
||||||
|
{
|
||||||
|
// Форма создания фильтра
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(): void
|
||||||
|
{
|
||||||
|
// Форма редактирования фильтра
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(): void
|
||||||
|
{
|
||||||
|
// Сохранение фильтра
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(): void
|
||||||
|
{
|
||||||
|
// Удаление фильтра
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.3.2. Новая модель: FilterModel (Admin)
|
||||||
|
|
||||||
|
**Путь:** `module/oc_telegram_shop/upload/admin/model/extension/tgshop/filter.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class ModelExtensionTgshopFilter extends Model
|
||||||
|
{
|
||||||
|
public function addFilter(array $data): int
|
||||||
|
{
|
||||||
|
$this->db->query("INSERT INTO " . DB_PREFIX . "telecart_filter
|
||||||
|
SET name = '" . $this->db->escape($data['name']) . "',
|
||||||
|
filter_json = '" . $this->db->escape(json_encode($data['filter_json'])) . "'");
|
||||||
|
|
||||||
|
return $this->db->getLastId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFilter(int $filterId): ?array
|
||||||
|
{
|
||||||
|
$query = $this->db->query("SELECT * FROM " . DB_PREFIX . "telecart_filter
|
||||||
|
WHERE filter_id = '" . (int)$filterId . "'");
|
||||||
|
|
||||||
|
return $query->row;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFilters(): array
|
||||||
|
{
|
||||||
|
$query = $this->db->query("SELECT * FROM " . DB_PREFIX . "telecart_filter
|
||||||
|
ORDER BY name ASC");
|
||||||
|
|
||||||
|
return $query->rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function editFilter(int $filterId, array $data): void
|
||||||
|
{
|
||||||
|
$this->db->query("UPDATE " . DB_PREFIX . "telecart_filter
|
||||||
|
SET name = '" . $this->db->escape($data['name']) . "',
|
||||||
|
filter_json = '" . $this->db->escape(json_encode($data['filter_json'])) . "'
|
||||||
|
WHERE filter_id = '" . (int)$filterId . "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteFilter(int $filterId): void
|
||||||
|
{
|
||||||
|
$this->db->query("DELETE FROM " . DB_PREFIX . "telecart_filter
|
||||||
|
WHERE filter_id = '" . (int)$filterId . "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.3.3. Новый view: Filter Builder
|
||||||
|
|
||||||
|
**Путь:** `module/oc_telegram_shop/upload/admin/view/template/extension/tgshop/filter.twig`
|
||||||
|
|
||||||
|
Создать интерфейс конструктора фильтров на основе существующего функционала из `Filters.vue`:
|
||||||
|
|
||||||
|
- Визуальный редактор для создания JSON фильтров
|
||||||
|
- Поддержка всех существующих типов фильтров:
|
||||||
|
- ProductPrice
|
||||||
|
- ProductCategory
|
||||||
|
- ProductManufacturer
|
||||||
|
- ProductModel
|
||||||
|
- ProductStatus
|
||||||
|
- ProductQuantity
|
||||||
|
- И другие
|
||||||
|
- Поле для ввода имени фильтра
|
||||||
|
- Кнопки: Сохранить, Удалить, Отменить
|
||||||
|
|
||||||
|
## 4. Схема работы
|
||||||
|
|
||||||
|
### 4.1. Создание баннера с фильтром (Администратор)
|
||||||
|
|
||||||
|
1. Администратор открывает конструктор фильтров в админ-панели
|
||||||
|
2. Создает фильтр (например: "Товары со скидкой в категории Электроника")
|
||||||
|
3. Сохраняет фильтр с именем `discount_electronics`
|
||||||
|
4. Переходит в настройки баннеров OpenCart
|
||||||
|
5. Создает новый слайд
|
||||||
|
6. Загружает изображение баннера
|
||||||
|
7. В поле "Ссылка" вводит: `#filter:discount_electronics`
|
||||||
|
8. Сохраняет баннер
|
||||||
|
|
||||||
|
### 4.2. Пользователь кликает на баннер (Frontend)
|
||||||
|
|
||||||
|
1. Пользователь видит баннер на главной странице
|
||||||
|
2. Кликает на баннер
|
||||||
|
3. YaMetrika фиксирует цель `BANNER_CLICK`
|
||||||
|
4. Frontend определяет тип ссылки: `filter`
|
||||||
|
5. Переход на страницу `/products-filter/discount_electronics`
|
||||||
|
6. Загрузка фильтра по имени `discount_electronics`
|
||||||
|
7. Применение фильтра к списку товаров
|
||||||
|
8. Отображение отфильтрованных товаров
|
||||||
|
|
||||||
|
### 4.3. Пример JSON фильтра
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"operand": "AND",
|
||||||
|
"rules": {
|
||||||
|
"RULE_PRODUCT_PRICE": {
|
||||||
|
"criteria": {
|
||||||
|
"product_price": {
|
||||||
|
"type": "number",
|
||||||
|
"params": {
|
||||||
|
"operator": "between",
|
||||||
|
"value": {
|
||||||
|
"from": 1000,
|
||||||
|
"to": 5000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RULE_PRODUCT_CATEGORY": {
|
||||||
|
"criteria": {
|
||||||
|
"product_category_id": {
|
||||||
|
"type": "product_category",
|
||||||
|
"params": {
|
||||||
|
"operator": "contains",
|
||||||
|
"value": 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Технические детали
|
||||||
|
|
||||||
|
### 5.1. Маршруты API
|
||||||
|
|
||||||
|
**Backend (PHP):**
|
||||||
|
|
||||||
|
- `GET /index.php?route=extension/tgshop/handle&api_action=filterList` - список фильтров
|
||||||
|
- `GET /index.php?route=extension/tgshop/handle&api_action=filterShow&name={name}` - получить фильтр
|
||||||
|
- `POST /index.php?route=extension/tgshop/handle&api_action=filterStore` - создать фильтр
|
||||||
|
- `POST /index.php?route=extension/tgshop/handle&api_action=filterUpdate` - обновить фильтр
|
||||||
|
- `POST /index.php?route=extension/tgshop/handle&api_action=filterDelete&id={id}` - удалить фильтр
|
||||||
|
|
||||||
|
**Admin Panel:**
|
||||||
|
|
||||||
|
- `GET /admin/index.php?route=extension/tgshop/filter` - список фильтров (страница)
|
||||||
|
- `GET /admin/index.php?route=extension/tgshop/filter/create` - форма создания
|
||||||
|
- `GET /admin/index.php?route=extension/tgshop/filter/edit&id={id}` - форма редактирования
|
||||||
|
- `POST /admin/index.php?route=extension/tgshop/filter/save` - сохранить фильтр
|
||||||
|
- `POST /admin/index.php?route=extension/tgshop/filter/delete&id={id}` - удалить фильтр
|
||||||
|
|
||||||
|
### 5.2. Валидация
|
||||||
|
|
||||||
|
- Имя фильтра: обязательно, уникальное, максимально 255 символов
|
||||||
|
- JSON фильтра: обязательно, валидный JSON, соответствует существующей схеме фильтров
|
||||||
|
- Проверка существования всех критериев в системе
|
||||||
|
|
||||||
|
### 5.3. Безопасность
|
||||||
|
|
||||||
|
- Проверка прав доступа для админ-панели
|
||||||
|
- SQL Injection защита через prepared statements
|
||||||
|
- XSS защита при выводе данных
|
||||||
|
- CSRF защита для форм администратора
|
||||||
|
|
||||||
|
### 5.4. Обратная совместимость
|
||||||
|
|
||||||
|
Использование формата ссылки `#filter:{filter_name}` с префиксом `#` гарантирует:
|
||||||
|
|
||||||
|
- **Совместимость с OpenCart**: При установке баннеров на обычный сайт OpenCart, якорная ссылка не приведет к ошибке 404, так как браузер просто оставит пользователя на текущей странице
|
||||||
|
- **Работа в SPA**: В Telegram веб-приложении JavaScript перехватывает клики и обрабатывает их специальным образом
|
||||||
|
- **Отказоустойчивость**: Если JavaScript не загрузился, пользователь не увидит ошибку, просто ничего не произойдет при клике
|
||||||
|
|
||||||
|
## 6. Интеграция с Яндекс.Метрикой
|
||||||
|
|
||||||
|
### 6.1. Цели метрики
|
||||||
|
|
||||||
|
Добавить в `spa/src/constants/yaMetrikaGoals.js`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
BANNER_CLICK: 'banner_click',
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2. События
|
||||||
|
|
||||||
|
- `banner_click` - клик по баннеру
|
||||||
|
- Параметры: `banner_title`, `link_type`, `filter_name`
|
||||||
|
|
||||||
|
### 6.3. Ecommerce события
|
||||||
|
|
||||||
|
При загрузке отфильтрованных товаров отправлять событие `view_item_list`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
yaMetrika.dataLayerPush({
|
||||||
|
ecommerce: {
|
||||||
|
items: products.map((product, index) => ({
|
||||||
|
item_id: product.id,
|
||||||
|
item_name: product.name,
|
||||||
|
price: product.final_price_numeric,
|
||||||
|
item_category: product.category_name,
|
||||||
|
quantity: 1,
|
||||||
|
discount: product.price_numeric - product.final_price_numeric,
|
||||||
|
})),
|
||||||
|
item_list_id: 'banner_filter',
|
||||||
|
item_list_name: 'Отфильтрованные товары',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. План реализации
|
||||||
|
|
||||||
|
### Этап 1: Backend (API для управления фильтрами)
|
||||||
|
|
||||||
|
- [ ] Создать таблицу `oc_telecart_filter`
|
||||||
|
- [ ] Создать модель `FilterModel`
|
||||||
|
- [ ] Создать хэндлер `FilterHandler` с методами CRUD
|
||||||
|
- [ ] Добавить маршруты в `routes.php`
|
||||||
|
- [ ] Обновить `BannerHandler` для поддержки фильтров
|
||||||
|
|
||||||
|
### Этап 2: Frontend (SPA компоненты)
|
||||||
|
|
||||||
|
- [ ] Создать компонент `ProductsFilter.vue`
|
||||||
|
- [ ] Обновить `Banner.vue` для обработки клика
|
||||||
|
- [ ] Добавить маршрут в `router.js`
|
||||||
|
- [ ] Добавить методы в `ftch.js`
|
||||||
|
- [ ] Добавить цель `BANNER_CLICK` в YaMetrika
|
||||||
|
|
||||||
|
### Этап 3: Admin Panel (Конструктор фильтров)
|
||||||
|
|
||||||
|
- [ ] Создать контроллер `FilterController` в админке
|
||||||
|
- [ ] Создать модель `ModelExtensionTgshopFilter`
|
||||||
|
- [ ] Создать шаблоны для списка и форм
|
||||||
|
- [ ] Реализовать визуальный конструктор фильтров
|
||||||
|
|
||||||
|
### Этап 4: Тестирование
|
||||||
|
|
||||||
|
- [ ] Unit тесты для модели фильтров
|
||||||
|
- [ ] Интеграционные тесты для API
|
||||||
|
- [ ] Функциональные тесты в SPA
|
||||||
|
- [ ] Тестирование интеграции с Яндекс.Метрикой
|
||||||
|
|
||||||
|
### Этап 5: Документация
|
||||||
|
|
||||||
|
- [ ] Документация для администраторов
|
||||||
|
- [ ] API документация
|
||||||
|
- [ ] Описание схемы JSON фильтров
|
||||||
|
|
||||||
|
## 8. Возможные расширения (будущее)
|
||||||
|
|
||||||
|
1. **Предпросмотр фильтров** - возможность увидеть количество товаров, соответствующих фильтру
|
||||||
|
2. **Предустановленные фильтры** - шаблоны популярных фильтров
|
||||||
|
3. **A/B тестирование баннеров** - ротация баннеров с отслеживанием конверсии
|
||||||
|
4. **Аналитика кликов** - статистика по каждому баннеру
|
||||||
|
5. **Многовариантные фильтры** - баннер может открывать несколько результатов
|
||||||
|
|
||||||
|
## 9. Примеры использования
|
||||||
|
|
||||||
|
### Пример 1: Баннер "Скидки до 50%"
|
||||||
|
|
||||||
|
**Фильтр:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"operand": "AND",
|
||||||
|
"rules": {
|
||||||
|
"RULE_PRODUCT_SPECIAL": {
|
||||||
|
"criteria": {
|
||||||
|
"has_special": {
|
||||||
|
"type": "boolean",
|
||||||
|
"params": {
|
||||||
|
"operator": "equals",
|
||||||
|
"value": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ссылка в баннере:** `#filter:specials_50_discount`
|
||||||
|
|
||||||
|
### Пример 2: Баннер "Новинки в категории"
|
||||||
|
|
||||||
|
**Фильтр:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"operand": "AND",
|
||||||
|
"rules": {
|
||||||
|
"RULE_PRODUCT_CATEGORY": {
|
||||||
|
"criteria": {
|
||||||
|
"product_category_id": {
|
||||||
|
"type": "product_category",
|
||||||
|
"params": {
|
||||||
|
"operator": "contains",
|
||||||
|
"value": 42
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RULE_PRODUCT_STATUS": {
|
||||||
|
"criteria": {
|
||||||
|
"product_status": {
|
||||||
|
"type": "boolean",
|
||||||
|
"params": {
|
||||||
|
"operator": "equals",
|
||||||
|
"value": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ссылка в баннере:** `#filter:new_in_electronics`
|
||||||
|
|
||||||
|
### Пример 3: Баннер "Товары до 1000₽"
|
||||||
|
|
||||||
|
**Фильтр:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"operand": "AND",
|
||||||
|
"rules": {
|
||||||
|
"RULE_PRODUCT_PRICE": {
|
||||||
|
"criteria": {
|
||||||
|
"product_price": {
|
||||||
|
"type": "number",
|
||||||
|
"params": {
|
||||||
|
"operator": "between",
|
||||||
|
"value": {
|
||||||
|
"from": 0,
|
||||||
|
"to": 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ссылка в баннере:** `#filter:budget_under_1000`
|
||||||
|
|
||||||
|
## 10. Выводы
|
||||||
|
|
||||||
|
Данное техническое задание описывает полную систему для реализации интерактивных баннеров с динамической фильтрацией
|
||||||
|
товаров. Реализация не нарушает существующую архитектуру проекта и полностью использует уже имеющийся функционал
|
||||||
|
фильтрации.
|
||||||
|
|
||||||
|
Основные преимущества:
|
||||||
|
|
||||||
|
- Гибкость настроек для администраторов
|
||||||
|
- Масштабируемость (можно добавить новые типы фильтров)
|
||||||
|
- Интеграция с Яндекс.Метрикой для аналитики
|
||||||
|
- Простота использования для администраторов магазина
|
||||||
|
- Обратная совместимость с существующими баннерами
|
||||||
|
- **Гарантированная совместимость с OpenCart**: использование якорных ссылок `#filter:{filter_name}` позволяет устанавливать баннеры как на SPA, так и на обычный сайт без ошибок
|
||||||
|
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
🎁 **ГЛАВНЫЕ НОВОСТИ:**
|
🎁 **ГЛАВНЫЕ НОВОСТИ:**
|
||||||
|
|
||||||
💰 **Поддержка купонов и ваучеров**
|
💰 **Поддержка купонов и подарочных сертификатов**
|
||||||
Telecart теперь поддерживает встроенную систему купонов OpenCart! Используйте нативные купоны и ваучеры для скидок и акций.
|
Telecart теперь поддерживает встроенную систему купонов OpenCart! Используйте нативные купоны и подарочных сертификаты для скидок и акций.
|
||||||
|
|
||||||
📊 **Улучшенная Яндекс.Метрика**
|
📊 **Улучшенная Яндекс.Метрика**
|
||||||
Теперь с поддержкой ecommerce, хитов и целей! Полная воронка продаж:
|
Теперь с поддержкой ecommerce, хитов и целей! Полная воронка продаж:
|
||||||
@@ -14,8 +14,8 @@ Telecart теперь поддерживает встроенную систем
|
|||||||
Уникальный ID покупателя позволяет анализировать поведение одного пользователя на всех устройствах.
|
Уникальный ID покупателя позволяет анализировать поведение одного пользователя на всех устройствах.
|
||||||
📖 Документация: https://telecart-labs.github.io/docs/analitycs/start/
|
📖 Документация: https://telecart-labs.github.io/docs/analitycs/start/
|
||||||
|
|
||||||
🖼️ **Баннеры на главной странице**
|
🖼️ **Баннеры с умной фильтрацией — продавайте то, что нужно именно сейчас**
|
||||||
Используйте встроенные баннеры OpenCart для акций и промо. Привлекайте внимание покупателей к важным предложениям!
|
Новинка для вашего Telegram-магазина! Теперь каждый баннер ведет клиента именно на те товары, которые вы хотите продвигать. Запустили акцию на скидки? Создайте баннер "Скидки до 50%" — и он автоматически покажет только товары со скидками. Нужно продать премиум-сегмент? Баннер "Товары от 1000₽" направит клиентов на нужный диапазон. Увеличивайте конверсию и контролируйте, какие товары видит покупатель после клика по баннеру. Встроенная фильтрация по цене, категориям, производителям делает ваши маркетинговые кампании максимально эффективными и результативными!
|
||||||
|
|
||||||
✏️ **Кастомизация текстов приложения**
|
✏️ **Кастомизация текстов приложения**
|
||||||
Теперь можно настраивать ключевые тексты:
|
Теперь можно настраивать ключевые тексты:
|
||||||
Reference in New Issue
Block a user