# Паттерн создания справочников: автоматизация статей, синонимов и валидации **Дата:** 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 для парсера - [ ] Написать тесты для валидации и парсинга