From 7e5b69c83b30712208583324cd538de20e6ea690 Mon Sep 17 00:00:00 2001 From: erjemin Date: Wed, 15 Jul 2026 23:20:34 +0300 Subject: [PATCH] =?UTF-8?q?doc:=20=D0=9F=D0=B0=D1=82=D1=82=D0=B5=D1=80?= =?UTF-8?q?=D0=BD=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BE=D1=87=D0=BD=D0=B8=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agent-doc/REFERENCE_ENTITIES_PATTERN.md | 595 ++++++++++++++++++++++++ 1 file changed, 595 insertions(+) create mode 100644 agent-doc/REFERENCE_ENTITIES_PATTERN.md diff --git a/agent-doc/REFERENCE_ENTITIES_PATTERN.md b/agent-doc/REFERENCE_ENTITIES_PATTERN.md new file mode 100644 index 0000000..1adc774 --- /dev/null +++ b/agent-doc/REFERENCE_ENTITIES_PATTERN.md @@ -0,0 +1,595 @@ +# Паттерн создания справочников: автоматизация статей, синонимов и валидации + +**Дата:** 2026-07-15 +**Статус:** Актуально ✓ + +## Обзор + +Система содержит 5 **справочных сущностей** (Reference Entities) которые описывают каталог: + +| Модель | Таблица | Назначение | Связь с TbArticle | +|-------------|--------------|------------------------------------------|-------------------| +| Лейбл | TbLabel | Издатель, лейбл, дистрибьютор | 1:1 OneToOne | +| Исполнитель | TbArtist | Артист, группа, бренд | 1:1 OneToOne | +| Товар | TbItem | Альбом, релиз, носитель, аксессуар | 1:1 OneToOne | +| Стиль | TbMusicStyle | Музыкальный жанр | 1:1 OneToOne | +| Продавец | TbSeller | Магазин, продавец, издатель-дистрибьютор | 1:1 OneToOne | + +Все 5 моделей используют **единый паттерн**: +1. Автоматическое создание связанной статьи (если нет) +2. Управление синонимами в метаданных +3. Валидация дубликатов через админку +4. Подготовка к парсингу с очередью конфликтов + +## Архитектура: TbArticle как связующее звено + +```text +┌─────────────────────────────────────────────────────────────┐ +│ TbArticle │ +│ (текст, SEO, картинка, слаг — для всех справочников) │ +│ │ +│ Поля: │ +│ • s_article_title: "Label: Sony" │ +│ • l_article_type: "label" │ +│ • slug: "label-sony" (URL-идентификатор) │ +│ • k_article_to_image: ссылка на картинку (логотип, фото) │ +│ • seo_title, seo_description: для поисковиков и llm │ +│ • s_article_teaser_html, s_article_content_html: текст │ +└─────────────────────────────────────────────────────────────┘ + ▲ ▲ ▲ ▲ + │ │ │ │ + 1:1 1:1 1:1 1:1 + │ │ │ │ +┌────────┴────┐ ┌────────┴─────┐ ┌────────┴────┐ ┌──┴──────────┐ +│ TbLabel │ │ TbArtist │ │ TbItem │ │ TbMusicStyle│ +│ │ │ │ │ │ │ │ +│ s_label │ │ s_artist │ │ s_item │ │ s_style_name│ +│ j_metadata │ │ j_metadata │ │ j_metadata │ │ j_metadata │ +│ (синонимы) │ │ (синонимы) │ │ (синонимы) │ │ (синонимы) │ +└─────────────┘ └──────────────┘ └─────────────┘ └─────────────┘ + +TbSeller аналогично, без синонимов (используется редко) +``` + +## Жизненный цикл справочника (на примере TbLabel) + +### Фаза 1: Создание в админке + +```python +# Админ создает новый лейбл через Django Admin форму +# Заполняет: s_label="Sony Music", j_label_metadata={"SYN_EN": [...]} + +# При сохранении форма вызывает label.save() +``` + +### Фаза 2: Валидация синонимов (админка) + +```python +# Функция validate_for_duplicates() проверяет: + +# 1. EXACT_MATCH: s_label == s_label в других записях? +# Результат: "Sony Music" уже есть в базе +# Действие: Форма показывает ошибку, блокирует сохранение + +# 2. FIND_IN_SYNONYM: s_label найден в синонимах других записей? +# Результат: "Sony Music" найден в синонимах записи TbLabel(pk=5) +# Действие: Админ видит список дубликатов, решает что делать + +# 3. EXACT_SYNONYM_MATCH: синонимы разных лейблов совпадают? +# Результат: {"SYN_EN": ["Sony", "Sony Music"]} уже есть в записи TbLabel(pk=3) +# Действие: Ошибка, нужно исправить синонимы +# +# 4. PARTIAL_MATCH: частичное совпадение синонимов +# Не критичные совпадения. s_label и синонимы из метаданных разбиваются по словам, ищутся совпадения слов. +# Админ видит предупреждение. Для коротких слов (Inc, Ltd т.п.) может много ложных срабатываний. Админу показывается +# список слов-совпадений, он с чем они совпали, и решает что делать. +``` + +### Фаза 3: Автоматическое создание статьи + +```python +# Если админ одобрил - вызывается label.save() + +def save(self, *args, **kwargs): + # Шаг 1: Валидация синонимов + validate_and_raise_for_duplicates(self, 's_label', 'j_label_metadata') + + # Шаг 2: Обновление синонимов метаданных + update_synonyms_in_metadata(self, 's_label', 'j_label_metadata') + + # Шаг 3: Создание или получение связанной статьи + article = create_or_get_related_article( + self, + TbArticle.ArticleType.LABEL, # тип статьи + 's_label', # основное поле + 'j_label_metadata', # метаданные + 'k_label_to_article' # FK на статью + ) + self.k_label_to_article = article + + # Шаг 4: Сохранение + super().save(*args, **kwargs) +``` + +**Что делает `create_or_get_related_article()`:** + +```python +def create_or_get_related_article( + instance, # TbLabel, TbArtist и т.д. + article_type, # TbArticle.ArticleType.LABEL + main_field_name, # 's_label' + metadata_field_name, # 'j_label_metadata' + fk_field_name # 'k_label_to_article' +): + # 1. Генерируем название для статьи + article_title = f"Label: {instance.s_label}" + + # 2. Ищем существующую статью + article = TbArticle.objects.filter( + s_article_title=article_title, + l_article_type=article_type + ).first() + + if article: + return article # Используем существующую + + # 3. Создаём новую статью + article = TbArticle.objects.create( + s_article_title=article_title, + l_article_type=article_type, + b_article_published=True, + # slug генерируется автоматически в TbArticle.save() + ) + + return article +``` + +## Синонимы: управление и валидация + +### Как формируются синонимы + +Синонимы хранятся в JSONField `j_*_metadata` в формате: + +```json +{ + "SYN_EN": [ + "The Beatles", + "Beatles", + "Beatles, The", + "The Fab Four" + ], + "other_data": {...} +} +``` + +### Автоматическое обновление синонимов при сохранении + +**Функция `update_synonyms_in_metadata()`:** + +```python +def update_synonyms_in_metadata(instance, main_field_name, metadata_field_name): + """ + При каждом сохранении: + 1. Если это новая запись: добавляем s_label в синонимы + 2. Если s_label изменился: добавляем оба (старый и новый) в синонимы + 3. Админ может редактировать синонимы через форму (приоритет админу) + """ + + current_value = getattr(instance, main_field_name) # "Sony Music" + original_value = instance._original_value # "Sony" + + # Получаем или создаём словарь метаданных + metadata = getattr(instance, metadata_field_name) or {} + synonyms = metadata.get(KEY_SYNONYM_EN, []) + + # Добавляем новые синонимы + if original_value and original_value != current_value: + # Изменилось название - добавляем оба + synonyms.extend([original_value, current_value]) + else: + # Новая запись - добавляем текущее + synonyms.append(current_value) + + # Удаляем дубликаты и короткие слова + synonyms = [s for s in set(synonyms) if len(s) >= MIN_SYNONYM_WORD_LENGTH] + + metadata[KEY_SYNONYM_EN] = sorted(synonyms) + setattr(instance, metadata_field_name, metadata) +``` + +### Валидация синонимов при сохранении + +```python +# Проверяем: все ли синонимы уникальны? + +validate_for_duplicates( + model_class=TbLabel, + instance_pk=label.id, + main_field_value=label.s_label, + metadata_dict=label.j_label_metadata, + main_field_name='s_label', + metadata_field_name='j_label_metadata', +) + +# Если найдены дубликаты (FIND_IN_SYNONYM, EXACT_SYNONYM_MATCH): +# - В админке: показываем форму с ошибкой + список дубликатов +# - В парсере (будущее): создаём запись в ValidatorQueue для админа +``` + +## Админка: как это видит пользователь + +### Создание нового лейбла + +``` +Админ заполняет форму: +┌────────────────────────────┐ +│ Название лейбла* │ +│ [ Sony Music ] │ +│ │ +│ Метаданные (JSON) │ +│ { "SYN_EN": [...] } │ +│ │ +│ [ Сохранить ] │ +└────────────────────────────┘ + +При сохранении: +1. Проверяются синонимы (EXACT_MATCH, FIND_IN_SYNONYM) +2. Если конфликт → показывается ошибка: + "Найдено совпадение! Существующие записи: [List]" + +3. Если ОК → статья создаётся автоматически + "Лейбл создан. Связанная статья: Label: Sony Music" +``` + +### Обновление существующего лейбла + +``` +Админ меняет название (например, опечатка): +[ Sony Musik ] → [ Sony Music ] + +1. Синонимы обновляются: добавляются оба варианта +2. Статья остаётся той же (slug не меняется) +3. Сохраняется историчность: "был Sony Musik, теперь Sony Music" +``` + +### Ошибки при сохранении + +``` +Сценарий: Админ пытается создать дубликат + +┌─────────────────────────────────────────────┐ +│ ОШИБКА: TbLabel.save() │ +│ Найдено совпадение в синонимах! │ +│ Разрешите на уровне админки. │ +│ PK конфликтующих записей: [1, 5, 12] │ +│ │ +│ Подробно: "Sony Music" найдено в │ +│ синонимах TbLabel(pk=5) │ +└─────────────────────────────────────────────┘ + +Решение: Админ либо меняет название, либо +объединяет с существующей записью вручную. +``` + +## Применение ко всем 5 моделям + +### TbLabel (Лейблы) + +```python +class TbLabel(models.Model): + s_label: str (уникальный) # "Sony Music", "Мелодия" + j_label_metadata: JSON # {"SYN_EN": [...]} + k_label_to_article: FK → TbArticle # автоматическое создание + + # ArticleType: LABEL + # Формат названия: "Label: Sony Music" +``` + +### TbArtist (Исполнители) + +```python +class TbArtist(models.Model): + s_artist: str (уникальный) # "The Beatles", "David Bowie" + j_artist_metadata: JSON # {"SYN_EN": [...]} + k_artist_to_article: FK → TbArticle # автоматическое создание + + # ArticleType: ARTIST + # Формат названия: "Artist: The Beatles" +``` + +### TbMusicStyle (Стили) + +```python +class TbMusicStyle(models.Model): + s_style_name: str (уникальный) # "Rock", "Jazz" + j_style_metadata: JSON # {"SYN_EN": [...]} + k_style_to_article: FK → TbArticle # автоматическое создание + + # ArticleType: STYLE + # Формат названия: "Style: Rock" +``` + +### TbItem (Товары/Релизы) + +```python +class TbItem(models.Model): + s_item: str (уникальный) # "Abbey Road (LP)" + j_item_metadata: JSON # {"SYN_EN": [...]} + k_item_to_article: FK → TbArticle # автоматическое создание + + # ArticleType: ITEM + # Формат названия: "Item: Abbey Road (LP)" +``` + +### TbSeller (Продавцы) + +```python +class TbSeller(models.Model): + s_seller: str (уникальный) # "Клюква Records", "Amazon" + j_seller_metadata: JSON # (синонимы не используются) + k_seller_to_article: FK → TbArticle # автоматическое создание + + # ArticleType: SELLER + # Формат названия: "Seller: Клюква Records" + # НЕ имеет синонимов (редко меняется, используется редко) +``` + +## Парсинг (будущее): интеграция с очередью конфликтов + +### Сценарий: парсер вытащил данные из Discogs + +```python +# Парсер получил из Discogs: +incoming_data = { + 's_artist': 'The Beatles', + 'j_artist_metadata': { + 'SYN_EN': ['Beatles', 'The Fab Four'], + 'DISCOGS_ID': 123456, + 'COUNTRY': 'UK', + 'FORMED': '1960', + } +} + +# Парсер пытается создать TbArtist(**incoming_data) +artist = TbArtist(**incoming_data) +artist.save() # Вызывает валидацию + +# Если есть конфликт (FIND_IN_SYNONYM, EXACT_SYNONYM_MATCH): +# - Парсер ловит ValidationError +# - Создаёт запись в ValidatorQueue +# - Админ получает уведомление про конфликт +# - Админ решает: создать новую / объединить / пропустить +``` + +### Структура ValidatorQueue для справочников + +```python +ValidatorQueue( + k_source=excel_file_source, + l_model_type='TbArtist', + l_conflict_type='FIND_IN_SYNONYM', # тип конфликта + model_pk=None, # это новая запись (или PK существующей) + + j_incoming_data={ + 's_artist': 'The Beatles', + 'j_artist_metadata': {'SYN_EN': [...], 'DISCOGS_ID': ...} + }, + + j_duplicates=[ + { + 'pk': 15, + 's_artist': 'Beatles', + 'matched_value': 'The Beatles', + 'match_type': 'FIND_IN_SYNONYM', + 'reason': 'Найдено в синонимах' + } + ], + + l_status='pending', # Ждёт решения админа + s_admin_decision=None, # Админ ещё не решил +) +``` + +### Решения админа + +``` +1. "create_new" — создать новую запись (с дубликатом) + Когда: Если это действительно разные сущности (например, + The Beatles (группа) vs The Beatles (альбом)) + +2. "merge" — объединить с существующей + Когда: Если это одна и та же сущность, только разные названия + Действие: Добавить синонимы в существующую запись + +3. "skip" — пропустить + Когда: Данные некорректны или дубликаты действительно + Действие: Ничего не создавать, просто пропустить +``` + +## Метаданные для парсинга: проверить, можно что-то вытащить из внешних источников + +### Из Discogs (для всех справочников) + +```json +{ + "DISCOGS_ID": 123456, // ID записи в Discogs + "DISCOGS_URL": "https://...", // Ссылка на Discogs + "DISCOGS_UPDATED": "2024-01-15",// Дата обновления на Discogs + "SYN_EN": [ // Синонимы на английском + "Alternative name", + "Another variant" + ] +} +``` + +### Для TbArtist (исполнители) — из Discogs, MusicBrainz + +```json +{ + "ARTIST_TYPE": "Group", // Solo, Group, Orchestra, Character + "FORMED": "1960", // Год образования + "DISBANDED": null, // Год распада (если есть) + "COUNTRY": "GB", // Страна (ISO 3166-1) + "AREA": "United Kingdom", // Регион + "MEMBERS": [ // Участники (для групп) + {"name": "John Lennon", "role": "Vocals"}, + {"name": "Paul McCartney", "role": "Bass"}, + ], + "WEBSITE": "https://beatles.com", // Официальный сайт + "WIKIPEDIA": "https://en.wikipedia.org/...", // Статья + "MUSICBRAINZ_ID": "uuid-here", // MusicBrainz ID + "BIOGRAPHY": "Краткая биография...", // Текст для статьи + "IMG_URL": "https://...", // Ссылка на изображение + "YANDEX_MUSIC_ID": 123456, // ID на Яндекс.Музыке + "SPOTIFY_ID": "spotify-id", // ID на Spotify + "SYN_EN": [...], + "SYN_RU": [...] // Синонимы на русском +} +``` + +### Для TbLabel (лейблы) — из Discogs, MusicBrainz + +```json +{ + "LABEL_TYPE": "Production", // Production, Reissue, Distributor + "FOUNDED": "1945", // Год основания + "COUNTRY": "USA", // Страна + "HEADQUARTERS": "New York, NY", // Штаб-квартира + "PARENT_LABEL": 456789, // ID материнского лейбла (Discogs) + "WEBSITE": "https://sony.com", // Официальный сайт + "WIKIPEDIA": "https://...", // Статья + "CONTACT_EMAIL": "info@...", // Email контакта + "CONTACT_PHONE": "+1-234-567", // Телефон + "COMPANY_SIZE": "Large", // Small, Medium, Large, Mega + "DISCOGS_CATALOG_PREFIX": "SR", // Префикс каталога + "IMG_LOGO": "https://...", // Логотип + "SYN_EN": [...], + "SYN_RU": [...] +} +``` + +### Для TbItem (релизы/альбомы) — из Discogs, MusicBrainz + +```json +{ + "ITEM_TYPE": "Album", // Album, Single, EP, Compilation + "RELEASE_DATE": "1969-09-26", // Дата выхода + "FORMAT": "Vinyl LP", // Vinyl LP, CD, Cassette, Digital + "COUNTRY": "GB", // Страна выхода + "BARCODE": "093624892340", // Штрихкод + "DISCOGS_ID": 987654, // ID мастер-релиза + "MUSICBRAINZ_ID": "uuid", // MusicBrainz ID + "GENRES": ["Rock", "Pop"], // Жанры (для синхронизации с TbMusicStyle) + "CATALOG_NUMBER": "SR-123", // Каталожный номер издателя + "LANGUAGE": "English", // Язык вокала/текста + "TRACKS_COUNT": 14, // Количество треков + "DURATION": "42:30", // Длительность альбома + "RATING": 4.5, // Рейтинг на Discogs (1-5) + "NOTES": "Limited edition...", // Примечания + "IMG_COVER": "https://...", // Обложка альбома + "YANDEX_MUSIC_ID": 123456, // ID на Яндекс.Музыке + "SPOTIFY_ID": "spotify-id", // ID на Spotify + "SYN_EN": [...], + "SYN_RU": [...] +} +``` + +### Для TbMusicStyle (жанры/стили) — из Discogs, MusicBrainz, Wikipedia + +```json +{ + "STYLE_TYPE": "Primary", // Primary, Sub-genre + "DESCRIPTION": "Rock music is...", // Описание жанра + "ORIGIN_COUNTRY": "USA", // Страна происхождения + "ORIGIN_YEAR": "1950s", // Период возникновения + "PARENT_GENRE": "Rock", // Основной жанр (для иерархии) + "RELATED_STYLES": [ // Связанные жанры + "Hard Rock", + "Progressive Rock" + ], + "NOTABLE_ARTISTS": [ // Известные исполнители + "The Beatles", + "Rolling Stones" + ], + "WIKIPEDIA": "https://...", // Статья в Википедии + "IMG_ILLUSTRATION": "https://...", // Иллюстрация для жанра + "SYN_EN": [...], + "SYN_RU": [...] +} +``` + +### Для TbSeller (продавцы) — из веб-скрейпинга, API или Excel (для excel и ручного ввода заполнять руками) + +```json +{ + "SELLER_TYPE": "Store", // Store, Label, Distributor, Marketplace + "FOUNDED": "2005", // Год основания + "COUNTRY": "RU", // Страна + "CITY": "Moscow", // Город + "ADDRESS": "ul. Pushkina, 42", // Адрес + "WEBSITE": "https://example.com", // Сайт + "EMAIL": "sales@example.com", // Email + "PHONE": "+7-495-123-45-67", // Телефон + "SOCIAL_VK": "https://vk.com/...", // VKontakte + "SOCIAL_INSTAGRAM": "https://...", // Instagram + "SHIPPING_COUNTRIES": [ // Страны доставки + "RU", "BY", "KZ" + ], + "PAYMENT_METHODS": [ // Методы оплаты + "Card", "Bank transfer", "Cash" + ], + "RETURNS_POLICY": "7 days", // Политика возврата + "RATING": 4.8, // Рейтинг на платформе + "REVIEWS_COUNT": 342, // Количество отзывов + "IMG_LOGO": "https://...", // Логотип магазина +} +``` + +## Жизненный цикл в нескольких слов + +``` +Админка Парсер (будущее) + │ │ + ├─ Заполняет форму ├─ Читает Discogs/Excel + │ (название, синонимы) │ (вытаскивает данные) + │ │ + ├─ Нажимает "Сохранить" ├─ Вызывает Model.save() + │ │ + ├─ Валидация синонимов ├─ Валидация синонимов + │ (FIND_IN_SYNONYM?) │ (конфликты?) + │ │ + ├─ Ошибка? → показать админу ├─ Конфликт? → ValidatorQueue + │ │ + ├─ OK? → создать статью ├─ OK? → создать модель + │ автоматически │ + статья автоматически + │ │ + └─ Данные целостны и готовы └─ Админ решает позже +``` + +## Преимущества подхода + +✓ **Единообразие**: 5 моделей работают по одному паттерну +✓ **Автоматизация**: Статьи создаются сами, не нужна ручная работа +✓ **Валидация**: Дубликаты и синонимы проверяются автоматически +✓ **Контроль**: Админ видит конфликты и может принять решение +✓ **Масштабируемость**: Легко добавить другие справочники (если нужны) +✓ **SEO-дружественно**: Каждый справочник имеет URL через слаг +✓ **История**: Изменения в названиях сохраняются через синонимы +✓ **Готовность к парсерам**: Структура поддерживает будущую интеграцию + +## Файлы реализации + +- **Модели**: `lpon_site/frontend/models.py` (TbLabel, TbArtist, TbItem, TbMusicStyle, TbSeller) +- **Валидаторы**: `lpon_site/frontend/utils_validators.py` (validate_for_duplicates) +- **Хелперы**: `lpon_site/frontend/utils.py` (create_or_get_related_article, update_synonyms_in_metadata) +- **Админка**: `lpon_site/frontend/admin.py` (LabelAdmin, ArtistAdmin и т.д.) +- **Парсер** (будущее): `lpon_site/parser/models.py` (ValidatorQueue), `tasks.py` (Celery задачи) + +## TODO: интеграция парсера + +- [ ] Создать Celery tasks для парсинга Discogs/Excel +- [ ] Реализовать ValidatorQueue с админ-интерфейсом +- [ ] Добавить поддержку очереди конфликтов в моделях +- [ ] Интегрировать сбор метаданных из Discogs API +- [ ] Документировать API для парсера +- [ ] Написать тесты для валидации и парсинга