docs: update docs

This commit is contained in:
2025-10-27 12:28:54 +03:00
parent 9ed6e54cba
commit 617b5491a1
2 changed files with 823 additions and 4 deletions

View 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, так и на обычный сайт без ошибок

View File

@@ -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₽" направит клиентов на нужный диапазон. Увеличивайте конверсию и контролируйте, какие товары видит покупатель после клика по баннеру. Встроенная фильтрация по цене, категориям, производителям делает ваши маркетинговые кампании максимально эффективными и результативными!
✏️ **Кастомизация текстов приложения** ✏️ **Кастомизация текстов приложения**
Теперь можно настраивать ключевые тексты: Теперь можно настраивать ключевые тексты: