AI и автоматизация

Управление SEO умного фильтра через ИИ-агента: полный гайд по автоматизации Aspro Smartseo

Управление SEO умного фильтра через ИИ-агента: полный гайд по автоматизации Aspro Smartseo

Кейс реального проекта: 354 посадочные страницы для каталога сэндвич-панелей на 1С-Битрикс + Аспро ЛайтШоп, созданные полностью через SSH-доступ ИИ-агентом, минуя графический интерфейс модуля.


Почему эта статья важна

Модуль «SEO умного фильтра» от Аспро (aspro.smartseo) — мощный инструмент для создания посадочных страниц под низкочастотные запросы умного фильтра. Но его графический интерфейс рассчитан на ручное создание правил и условий по одному. Когда вам нужно развернуть сотни посадочных страниц с разными свойствами, разделами и SEO-тегами — ручной путь становится невыносимым:

  • 8 разделов × 5 свойств × десятки значений = 300+ посадочных
  • Каждую нужно заполнить: Title, Description, H1, Keywords, SEO-текст на 3000 символов
  • Каждое правило в UI требует 15-20 кликов и аккуратного заполнения формы
  • Любая опечатка в CONDITION_TREE ломает генерацию URL без понятной ошибки

В этом гайде — как делегировать эту работу ИИ-агенту, который работает с БД напрямую через ORM и SQL.


Архитектура решения

┌─────────────────────────────────────────────────────────┐
│                  ИИ-агент (ZCode/CLI)                    │
│  - Читает ТЗ (XLSX/CSV/DOCX)                             │
│  - Формирует JSON-дампы                                  │
│  - Генерирует PHP-скрипты                                │
└────────────────────┬────────────────────────────────────┘
                     │ SSH (ключи)

┌─────────────────────────────────────────────────────────┐
│              Bitrix-сервер (PHP 8.2 CLI)                 │
│  - prolog_before.php (bootstrap)                         │
│  - CModule::IncludeModule('aspro.smartseo')              │
│  - Прямой $DB->Query для low-level операций             │
│  - ORM Aspro\Smartseo\Models\* для бизнес-логики         │
└─────────────────────────────────────────────────────────┘

Ключевой принцип: никакого браузера, никакого графического интерфейса. Всё через CLI.


Что нужно знать об устройстве aspro.smartseo

Модуль хранит данные в 18 таблицах с префиксом b_aspro_smartseo_*. Главные из них:

ТаблицаНазначение
b_aspro_smartseo_filter_ruleПравила (rule = связка сайт+инфоблок+разделы)
b_aspro_smartseo_filter_iblock_sectionsПривязка разделов к правилу (ManyToMany)
b_aspro_smartseo_filter_conditionУсловия (condition = фильтр по свойствам)
b_aspro_smartseo_filter_urlСгенерированные URL (NEW_URL ↔ REAL_URL)
b_aspro_smartseo_seo_templateSEO-шаблоны (Title/Description/…)

Иерархия сущностей

FilterRule (FR)
  ├── IBLOCK_SECTIONS (разделы, к которым применяется)
  └── FilterCondition (FC) — условия генерации
        └── FilterUrl (FU) — сгенерированные URL
              └── SeoTemplate (FU/FC/FR) — SEO-теги и тексты

SEO-шаблоны можно навешивать на любой уровень: правило целиком, условие, конкретный URL. Чем ниже уровень — тем выше приоритет.

8 типов SEO-полей

CODEЧто это
META_TITLEМета-тег <title>
META_DESCRIPTIONМета-тег description
META_KEYWORDSМета-тег keywords
PAGE_TITLEH1 страницы
BREADCRUMB_PAGEХлебные крошки
TOP_DESCRIPTIONТекст над каталогом
BOTTOM_DESCRIPTIONТекст под каталогом (основной SEO-текст)
ADDITIONAL_DESCRIPTIONДополнительный блок

Типы сущностей для SEO-шаблонов

КодТипПрименение
FRFilterRuleВсе URL правила (общий шаблон)
FCFilterConditionВсе URL условия
FUFilterUrlКонкретный URL (точечная настройка)

Подготовка каталога

1. Свойства — только список (L), не число (N)

Критический момент: умный фильтр требует свойства типа «Список» (L) с pre-определёнными значениями в b_iblock_property_enum. Свойства типа «Число» (N) не работают для генерации посадочных — движок просто не увидит значения.

// ПРАВИЛЬНО
$ibp = new CIBlockProperty;
$ibp->Add([
    'NAME' => 'Толщина',
    'CODE' => 'TOLSHCHINA_MM',
    'IBLOCK_ID' => 85,
    'PROPERTY_TYPE' => 'L',          // ← список
    'LIST_TYPE' => 'L',
    'MULTIPLE' => 'N',
    'FILTRABLE' => 'Y',
    'VALUES' => [
        ['VALUE' => '40',  'XML_ID' => md5('40'),  'SORT' => 40],
        ['VALUE' => '50',  'XML_ID' => md5('50'),  'SORT' => 50],
        // ... 13 значений от 40 до 250 мм
    ],
]);

2. Включение умного фильтра для разделов

Aspro.smartseo v1.0.8 читает свойства из старой таблицы b_iblock_section_property (с колонкой SMART_FILTER), а НЕ из новой b_iblock_property_feature. Это важно: даже если вы включили умный фильтр в админке, нужно убедиться, что запись есть в старой таблице.

// SECTION_ID=0 = "для всех разделов инфоблока"
\Bitrix\Iblock\SectionPropertyTable::update(
    ['IBLOCK_ID' => 85, 'SECTION_ID' => 0, 'PROPERTY_ID' => $propId],
    ['SMART_FILTER' => 'Y', 'DISPLAY_TYPE' => 'F', 'DISPLAY_EXPANDED' => 'Y']
);

DISPLAY_TYPE управляет визуализацией:

  • F — флажки/чекбоксы (для перечислений)
  • G — с картинкой/цветом (для RAL)
  • K — радио-кнопки
  • P — числовой диапазон

3. Товары-болванки

Aspro НЕ генерирует URL для значений фильтра, под которыми нет товаров. Если вы добавили в enum значение «120 мм», но в каталоге нет ни одного товара с этим значением — посадочная не создастся.

Решение: создайте матрицу товаров-болванок, покрывающую все значения всех свойств. Минимум — max(количество значений по свойствам) товаров на раздел.

// 22 товара на раздел (по количеству RAL-цветов) — покрывает все 13 толщин,
// 2 ширины, 22 RAL, 2 цвета, 6 типов профилирования
// Каждому товару даём циклически значения всех свойств
for ($i = 0; $i < 22; $i++) {
    $ralIdx  = $i;                              // 0..21
    $tolIdx  = $i % 13;                         // циклически 0..12
    $shirIdx = $i % 2;
    // ... создаём товар с этими значениями
}

CONDITION_TREE — главный формат

Это сердце модуля. Дерево условий хранится в b_aspro_smartseo_filter_condition.CONDITION_TREE как PHP-serialized массив.

Рабочий шаблон

$tree = [
    'CLASS_ID' => 'CondGroup',
    'DATA' => ['All' => 'AND'],                  // AND или OR
    'CHILDREN' => [
        [
            'CLASS_ID' => 'CondIBProp:85:1950',  // CondIBProp:<IBLOCK>:<PROPERTY>
            'DATA' => [
                'logic' => 'Equal',              // Equal, Not, Contain, ...
                'value' => 748,                  // ← ID enum-значения (НЕ само значение!)
            ],
        ],
        [
            'CLASS_ID' => 'CondIBProp:85:1950',
            'DATA' => ['logic' => 'Equal', 'value' => 749],  // ещё значение
        ],
        // ... сколько угодно детей = ИЛИ внутри одного свойства
    ],
];

$conditionTree = serialize($tree);

Ключевые нюансы

  1. value — это enum ID (из b_iblock_property_enum.ID), а не само значение «40»
  2. CLASS_ID имеет формат CondIBProp:<IBLOCK_ID>:<PROPERTY_ID> — обе цифры обязательны
  3. Несколько детей одного свойства = логика ИЛИ (комбинирование)
  4. Разные свойства в детях = логика И (пересечение)

Создание условия через ORM

use Aspro\Smartseo\Models\SmartseoFilterConditionTable;

$result = SmartseoFilterConditionTable::add([
    'FILTER_RULE_ID'       => $ruleId,
    'ACTIVE'               => 'Y',
    'NAME'                 => 'Толщина',
    'URL_TEMPLATE'         => '#SITE_DIR#catalog/#SECTION_CODE_PATH#-{#PROPERTY_VALUE#}-mm/',
    'URL_TYPE_GENERATE'    => 'CM',     // CM = комбинирование, MR = слияние
    'URL_STRICT_COMPLIANCE'=> 'N',
    'CONDITION_TREE'       => serialize($tree),
]);

$conditionId = $result->getId();

URL_TEMPLATE — токены

ТокенЧто подставляется
#SITE_DIR#Корень сайта (/)
#SECTION_CODE_PATH#Путь из кодов разделов (catalog/sendvich-paneli/stenovye-sendvich-paneli)
#SECTION_CODE#Код текущего раздела
{#PROPERTY_VALUE#}Значение свойства (в фигурных скобках!)
{#PROPERTY_CODE#}Код свойства
#SMART_FILTER_PATH#Стандартный путь умного фильтра

Важно: {#PROPERTY_VALUE#} берётся в фигурные скобки, а остальные токены — в решётки. Это не опечатка, так задумано.

URL_TYPE_GENERATE

  • CM (Комбинирование) — для каждого значения свойства отдельный URL
  • MR (Слияние) — все значения в один URL

Для посадочных под НЧ-запросы почти всегда нужен CM.


Генерация URL через UrlEngine

Самый приятный момент — после создания условий не нужно вручную что-либо генерировать. Движок сам обходит товары, находит совпадения с CONDITION_TREE и создаёт записи в b_aspro_smartseo_filter_url.

use Aspro\Smartseo\Engines\UrlEngine;

$engine = new UrlEngine($conditionId);
$engine->update();

$result = $engine->getResult();
echo "Сгенерировано URL: " . $result['COUNT'];

Движок сам:

  1. Обходит разделы правила
  2. Применяет CONDITION_TREE как фильтр к товарам
  3. Для каждой комбинации «раздел + значение свойства» создаёт URL
  4. Записывает в БД пару NEW_URL (ЧПУ) ↔ REAL_URL (стандартный путь умного фильтра)

Заливка SEO-тегов и текстов

Структура записей

// INSERT в b_aspro_smartseo_seo_template
// для каждого URL и каждого SEO-полей
foreach ($urls as $urlId => $tags) {
    foreach (['META_TITLE', 'META_DESCRIPTION', 'META_KEYWORDS', 'PAGE_TITLE'] as $code) {
        $DB->Query("INSERT INTO b_aspro_smartseo_seo_template
            (CODE, TEMPLATE, ENTITY_TYPE, ENTITY_ID)
            VALUES ('$code', '" . $DB->ForSql($tags[$code]) . "', 'FU', $urlId)");
    }
}

Теги региональности

Aspro LiteShop поддерживает теги для подстановки названия города:

ТегПадежПример (Москва)
#REGION_NAME#ИменительныйМосква
#REGION_NAME_DECLINE_RP#РодительныйМосквы
#REGION_NAME_DECLINE_PP#ПредложныйМоскве
#REGION_NAME_DECLINE_TP#ТворительныйМосквой

Эти теги работают в любом SEO-поле, включая BOTTOM_DESCRIPTION. Aspro заменяет их на лету при отдаче страницы — но только если в модуле «Аспро → Региональность» настроены города со склонениями.

Умная замена плейсхолдеров

Если SEO-тексты приходят от копирайтера с универсальным плейсхолдером {город} — нужно автоматически распределить теги по контексту:

import re

def replace_gorod(text):
    # "в {город}" / "В {город}" → предложный (в Москве)
    text = re.sub(r'\b([вВ])\s+\{город\}', r'\1 #REGION_NAME_DECLINE_PP#', text)
    # "для {город}" → родительный (для Москвы)
    text = re.sub(r'\b(для|Для)\s+\{город\}', r'\1 #REGION_NAME_DECLINE_RP#', text)
    # "с {город}" / "над {город}" → творительный (с Москвой)
    text = re.sub(r'\b(с|С|над|под|перед)\s+\{город\}', r'\1 #REGION_NAME_DECLINE_TP#', text)
    # Оставшиеся без предлога → именительный (Москва)
    text = text.replace('{город}', '#REGION_NAME#')
    return text

В нашем проекте 98% случаев были «в/В {город}» → предложный падеж. Это дало корректные фразы «купить в Москве», «доставка в Москве», «специалисты в Москве помогут».


Ошибки, с которыми мы столкнулись

1. Bitrix prolog ругается на output

Симптом: скрипт обрывается сообщением «При выполнении скрипта возникла ошибка. Включить расширенный вывод ошибок…»

Причина: prolog_after.php пытается отправить HTTP-заголовки, а вы уже вывели echo. На CLI это фатально.

Решение: используйте файловый лог вместо echo.

$LOG = __DIR__ . '/script.log';
file_put_contents($LOG, "");  // очищаем в начале
function log_s($m) {
    global $LOG;
    file_put_contents($LOG, date("H:i:s") . " " . $m . "\n", FILE_APPEND);
}

// Вместо echo:
log_s("Создано записей: $count");

2. Свойство не находится в ConditionResult

Симптом: ConditionResult::getPropertyFields() возвращает пустой массив для свойства, хотя оно явно существует с типом L.

Причина: после конвертации свойства из N в L (UPDATE b_iblock_property SET PROPERTY_TYPE='L' WHERE ID=...) aspro кэширует старое состояние свойства. Свойство видно в БД, но движок его игнорирует.

Решение: полное пересоздание свойства через CIBlockProperty::Add().

// 1) Сохраняем enum и значения товаров
$enums = $DB->Query("SELECT * FROM b_iblock_property_enum WHERE PROPERTY_ID=$oldId")->fetchAll();
$values = $DB->Query("SELECT * FROM b_iblock_element_property WHERE IBLOCK_PROPERTY_ID=$oldId")->fetchAll();

// 2) Удаляем полностью
$DB->Query("DELETE FROM b_iblock_property WHERE ID=$oldId");
$DB->Query("DELETE FROM b_iblock_property_enum WHERE PROPERTY_ID=$oldId");
$DB->Query("DELETE FROM b_iblock_element_property WHERE IBLOCK_PROPERTY_ID=$oldId");
$DB->Query("DELETE FROM b_iblock_section_property WHERE PROPERTY_ID=$oldId");

// 3) Создаём заново
$ibp = new CIBlockProperty;
$newId = $ibp->Add([
    'NAME' => 'Ширина',
    'CODE' => 'SHIRINA_MM',
    'IBLOCK_ID' => 85,
    'PROPERTY_TYPE' => 'L',
    'LIST_TYPE' => 'L',
    'VALUES' => [...],
]);

// 4) Возвращаем значения товарам с новыми enum ID
// 5) Включаем SMART_FILTER заново

После этого aspro видит свойство и генерирует URL. Это самый неочевидный баг, на который мы потратили часы.

3. 0 URL сгенерировано, но товары есть

Симптом: UrlEngine->update() возвращает COUNT=0, хотя CONDITION_TREE корректный и товары со значениями есть.

Причина: либо aspro не видит свойство (см. пункт 2), либо нет товаров, удовлетворяющих условию в конкретном разделе.

Диагностика:

// Проверяем, видит ли ConditionResult свойство
$cr = new \Aspro\Smartseo\Condition\ConditionResult(85, $conditionTreeSerialized);
$fields = $cr->getPropertyFields();
// Если пусто — aspro не нашёл свойство, проблема в кэше типов

4. Двойные префиксы в URL

Симптом: вместо /catalog/...-ral-1014/ получается /catalog/...-ral-ral-1014-slonovaya-kost/.

Причина: aspro берёт VALUE enum и транслитерирует его. Если VALUE = «RAL 1014 (слоновая кость)» — в URL попадёт весь текст.

Решение: храните в VALUE короткое значение, а расшифровку — в отдельном поле или в описании.

-- Было:
UPDATE b_iblock_property_enum SET VALUE='RAL 1014 (слоновая кость)' WHERE ID=718;
-- Стало:
UPDATE b_iblock_property_enum SET VALUE='1014' WHERE ID=718;

И в URL_TEMPLATE оставьте префикс:

#SITE_DIR#catalog/#SECTION_CODE_PATH#-ral-{#PROPERTY_VALUE#}/

5. Опечатки в enum ломают URL

Aspro использует стандартный битриксовый транслит, который не прощает опечаток. «Трапецевидная» → trapets**evi**dnaya, а в ТЗ от SEO-специалиста могло быть trapecevidnaya (без ts).

Решение: либо менять enum VALUE под битриксовый транслит, либо настраивать правила транслитерации. Проще первое.

6. ORM cache ломает данные

После любого INSERT в b_aspro_smartseo_seo_template обязательно сбрасывайте кэш:

$BX_CACHE = \Bitrix\Main\Application::getInstance()->getManagedCache();
$BX_CACHE->cleanDir('aspro_smartseo');
$BX_CACHE->cleanDir('b_iblock');

// И физическое удаление файлов кэша
shell_exec('rm -rf ' . $_SERVER['DOCUMENT_ROOT'] . '/bitrix/managed_cache/MYSQL/*');
shell_exec('rm -rf ' . $_SERVER['DOCUMENT_ROOT'] . '/bitrix/cache/*');

Иначе вы увидите старые SEO-теги на странице даже после заливки новых.

7. SITE_ID в правиле решает

Правило привязывается к SITE_ID. Если у вас многосайтовость — правило для s1 НЕ будет работать на s2. Нужно создавать отдельное правило для каждого сайта или убедиться, что целевой домен использует нужный SITE_ID.

В нашем случае dev.all4stroika.ru использует SITE_ID=s2 — и это боевой сайт, несмотря на название.


Фишки, которые теперь доступны через ИИ-агента

1. Массовое добавление значений свойств

Нужно добавить новые толщины (300, 350 мм)? Раньше — кликать в админке. Теперь:

foreach ([300, 350] as $v) {
    $DB->Query("INSERT INTO b_iblock_property_enum
        (PROPERTY_ID, VALUE, XML_ID, SORT, DEF)
        VALUES (1950, '$v', '" . md5($v) . "', $v, 'N')");
}
// Перегенерация URL
$engine = new UrlEngine(5);
$engine->update();

30 секунд вместо 10 минут в UI.

2. Массовое обновление SEO-тегов

Получили от копирайтера обновлённые Title для 100 страниц? Заливаем одной транзакцией:

$DB->Query("DELETE FROM b_aspro_smartseo_seo_template
    WHERE ENTITY_TYPE='FU' AND CODE='META_TITLE'
    AND ENTITY_ID IN (" . implode(',', $urlIds) . ")");

foreach ($newTitles as $urlId => $title) {
    $DB->Query("INSERT INTO b_aspro_smartseo_seo_template
        (CODE, TEMPLATE, ENTITY_TYPE, ENTITY_ID)
        VALUES ('META_TITLE', '" . $DB->ForSql($title) . "', 'FU', $urlId)");
}

3. Автоматическая генерация SEO-текстов под новые посадочные

Если вы добавили новое значение свойства — ИИ-агент может сгенерировать SEO-текст по шаблону существующих текстов и сразу залить:

# Агент читает существующие тексты, изучает структуру,
# генерирует новый по аналогии и подставляет теги регионов
template_text = generate_text_for_new_thickness(300)  # AI-generated
text = replace_gorod(template_text)  # подставляем #REGION_NAME_*#

4. Тестирование изменений без риска

Все скрипты идемпотентны: их можно запускать многократно. Перед запуском на проде — сухой прогон с echo SQL-запросов вместо $DB->Query(). После проверки — боевой запуск.

5. Дамп конфигурации для аудита

Любой момент времени можно задампить всю конфигурацию в JSON:

$config = [
    'rules' => [...],
    'conditions' => [...],
    'urls' => [...],
    'seo_templates' => [...],
];
file_put_contents('audit_' . date('Y-m-d') . '.json', json_encode($config, JSON_PRETTY_PRINT));

И сравнить с предыдущим дампом через diff — увидите все изменения.

6. Автоматический матчинг ТЗ и БД

ТЗ от SEO-специалиста приходит в Excel/CSV. ИИ-агент:

  1. Парсит файл
  2. Сравнивает URL из ТЗ с URL в БД
  3. Находит расхождения (новые посадочные, удалённые, переименованные)
  4. Догенерирует недостающее
  5. Обновляет изменённые теги

Всё без ручной сверки таблиц.


Безопасная работа на проде

1. Бэкап перед изменениями

# Дамп ключевых таблиц
mysqldump -u user -p db \
  b_aspro_smartseo_filter_rule \
  b_aspro_smartseo_filter_condition \
  b_aspro_smartseo_filter_url \
  b_aspro_smartseo_seo_template \
  b_iblock_property b_iblock_property_enum \
  b_iblock_section_property \
  > backup_$(date +%Y%m%d_%H%M).sql

2. Доступ только через SSH-ключи

Никаких паролей в скриптах. SSH-ключи на сервере, права chmod 600.

3. Идемпотентные скрипты

Каждый скрипт должен:

  • Сначала проверять, существует ли уже запись
  • Если да — обновлять, если нет — создавать
  • Не падать на повторном запуске
$exist = CIBlockElement::GetList(
    [],
    ['IBLOCK_ID' => 85, 'CODE' => $code],
    false, false, ['ID']
)->Fetch();

if ($exist) {
    CIBlockElement::SetPropertyValuesEx($exist['ID'], 85, $props);
    // обновляем
} else {
    $el->Add($fields);  // создаём
}

4. Логирование каждого шага

Все скрипты пишут в /home/.../scripts_seo/stepN.log. Если что-то пошло не так — у вас полный аудит-лог.


Чеклист для запуска на новом проекте

  • Настроен SSH-доступ по ключам
  • Найден PHP для CLI (для Битрикс 26 нужен 8.2+)
  • Определён IBLOCK_ID целевого каталога
  • Определён SITE_ID целевого домена
  • Свойства созданы с типом L (не N, не S)
  • SMART_FILTER=Y для всех нужных разделов (в старой таблице b_iblock_section_property)
  • Созданы товары-болванки, покрывающие все значения свойств
  • Сброшен кэш after каждого шага
  • Сделан бэкап таблиц aspro.smartseo перед началом
  • Настроен модуль aspro.lite regions с городами и склонениями

Итог

ИИ-агент через SSH — это не «обход админки». Это продуктивный интерфейс для массовых операций с aspro.smartseo.

Что мы получили:

  • 354 посадочные страницы за 1 день работы (вместо недели ручного кликанья)
  • 1705 SEO-тегов + 317 SEO-текстов (3000+ символов каждый)
  • Корректные теги региональности во всех полях
  • Полная история изменений в git
  • Идемпотентные скрипты, которые можно переиспользовать

Что стало возможным:

  • Добавить новое значение свойства → 30 секунд на заливку и перегенерацию
  • Обновить 100 Title → один SQL-запрос
  • Провести аудит → один SELECT
  • Откатить изменения → один mysqldump-restore

Графический интерфейс остаётся для единичных правок и визуализации. Для всего остального — есть SSH.


Приложение: полезные SQL-запросы

Сводка по правилам и условиям

SELECT
    r.ID as rule_id, r.NAME as rule_name, r.SITE_ID,
    COUNT(DISTINCT c.ID) as conditions,
    COUNT(DISTINCT u.ID) as urls
FROM b_aspro_smartseo_filter_rule r
LEFT JOIN b_aspro_smartseo_filter_condition c ON c.FILTER_RULE_ID = r.ID
LEFT JOIN b_aspro_smartseo_filter_url u ON u.FILTER_CONDITION_ID = c.ID
GROUP BY r.ID;

Найти URL без SEO-тегов

SELECT u.NEW_URL
FROM b_aspro_smartseo_filter_url u
LEFT JOIN b_aspro_smartseo_seo_template t
    ON t.ENTITY_TYPE='FU' AND t.ENTITY_ID=u.ID
WHERE t.ID IS NULL;

Сравнить SITE_ID правила с фактическим SITE_ID домена

SELECT r.ID, r.NAME, r.SITE_ID, s.NAME as site_name, s.SERVER_NAME
FROM b_aspro_smartseo_filter_rule r
LEFT JOIN b_lang s ON s.LID = r.SITE_ID;

Все URL с их SEO-тегами

SELECT
    u.NEW_URL,
    MAX(CASE WHEN t.CODE='META_TITLE' THEN t.TEMPLATE END) as title,
    MAX(CASE WHEN t.CODE='PAGE_TITLE' THEN t.TEMPLATE END) as h1,
    MAX(CASE WHEN t.CODE='META_DESCRIPTION' THEN t.TEMPLATE END) as description
FROM b_aspro_smartseo_filter_url u
LEFT JOIN b_aspro_smartseo_seo_template t
    ON t.ENTITY_TYPE='FU' AND t.ENTITY_ID=u.ID
GROUP BY u.ID;

Кейс по проекту all4stroika.ru, июль 2026. Автор — Евгений Урядов, automata.sale: 20+ лет опыта с 1С-Битрикс, партнёр Аспро. Полный набор идемпотентных PHP-скриптов и ТЗ доступен в git-репозитории проекта.

🚀 Нужна помощь с сайтом на 1С-Битрикс или Аспро?

Я работаю удалённо по всей России и СНГ. Узнайте цены и условия для вашего города:

Все регионы →