minor: переименование

This commit is contained in:
2026-07-23 23:16:25 +03:00
parent 4dd34d5550
commit 18ff09fe3d
5 changed files with 0 additions and 0 deletions
+435
View File
@@ -0,0 +1,435 @@
## ПОТОК ДАННЫХ (чистая архитектура) ДРАФТ
```text
1️⃣ ИСТОЧНИК ДОБАВЛЕН
├─ Пользователь добавляет новый источник (TbSource)
└─ Создается задание в Redis: `job:parse_source:123`
2️⃣ ФОНОВЫЙ ВОРКЕР (Celery/APScheduler)
├─ Видит задание в Redis
├─ Начинает парсить (не пишет в prod БД!)
├─ Получает: Style["Rock"], Artist["The Beatles"], Label["Sony"]
├─ Кидает КАЖДЫЙ в Redis как **виртуальный объект**
│ └─ `pending:style:1` → {"title": "Rock", "source": "Discogs", ...}
│ └─ `pending:artist:2` → {"title": "The Beatles", "source": "MusicBrainz", ...}
└─ Смотрит: есть ли в product БД? Если НЕТ → в Redis очередь
3️⃣ АДМИНКА ДЖАНГО (виртуальная вкладка "Ожидающие задачи")
├─ Админ видит в Redis список
├─ Видит: Rock (похож на existing Rock?), The Beatles (новый?), Sony (уже есть)
├─ **ПОТЫКАЛ**:
│ ├─ Rock → "Мержить с existing Rock" → задание в Redis
│ ├─ The Beatles → "Одобрить" → задание в Redis
│ └─ Sony → "Пропустить" → удалить из Redis
└─ Каждое действие → новое задание в Redis
4️⃣ ФОНОВЫЙ ВОРКЕР II (финальный штрих)
├─ Видит задание: "merge Rock с ID:5"
│ └─ Делает: добавляет синонимы, сохраняет в product БД
├─ Видит задание: "create The Beatles"
│ └─ Пишет вреальную БД → TbArtist создана
└─ После каждого успеха → **удалить из Redis**
5️⃣ ПРОДАКТ БД (в итоге)
└─ Только валидные, одобренные, обработанные данные
└─ No garbage, ID подряд
```
# Архитектура
```text
Парсер Redis (очередь) Админка Воркер БД
│ │ │ │ │
├──parse_source───────────→│ │ │ │
│ │ │ │ │
│ ┌─pending:style:1 │ │ │
│ ├─pending:artist:1 ←─ Админ видит видит здесь │ │
│ └─pending:label:1 потыкает кнопки │ │
│ │ │ │ │
│ │ "merge Rock"│ │ │
│ │←───────────────────│ →─ job:merge:1 ──────→│ │
│ │ │ ├─→ UPDATE Style →│
│ │ │ │ delete from Redis
│ │ "create Beatles" │ │
│ │←──────────────────→─ job:create:2 ────────→│ │
│ │ │ ├─→ INSERT Artist ─│
│ │ │ │ delete from Redis
│ │ │ │ │
└───────────────────────────────────────────────────────────────────────────────────────────────────→
```
---
## КЛЮЧЕВЫЕ ПРИНЦИПЫ
- **Чистая БД**: product база получает только одобренные, валидные данные
- **ID подряд**: мнимизирукет удаления (delete), INSERT данных парсинга только при одобрении
- **Single Source of Truth**: пока не одобрено администратором → данные ТОЛЬКО в Redis
- **Асинхронность**: парсер не блокирует админку, админка не блокирует парсер
- **Откат дешевый**: удалить из Redis дешевле, чем восстанавливать из БД
---
## КОМПОНЕНТЫ REDIS ОЧЕРЕДИ
```
tasks:pending ← Очередь неодобренных задач (парсер → сюда)
└─ pending:style:1
└─ pending:artist:2
└─ pending:label:3
tasks:approved ← Очередь одобренных (админ → сюда)
└─ {type: 'create', id: 'style:1', data: {...}}
└─ {type: 'merge', id: 'artist:1', merge_with_id: 5}
└─ {type: 'skip', id: 'label:3'}
tasks:completed ← История завершённых (воркер → сюда)
tasks:failed ← История ошибок (воркер → сюда)
```
---
## ДЕЙСТВИЯ В АДМИНКЕ (виртуальная вкладка)
Админ видит Redis очередь и кликает:
1. **"✅ Одобрить"** → `job:create:style:1` → Воркер пишет в БД
2. **"🔗 Мержить с ID:5"** → `job:merge:style:1:with:5` → Воркер обновляет existing
3. **"❌ Пропустить"** → Удалить из Redis (ничего не пишется)
4. **"📝 Отредактировать"** → Изменить JSON в оптимистичной форме → сохранить как новое задание
---
## ПАРСЕР (упрощённо)
```python
def parse_source(source_id):
source = TbSource.objects.get(id=source_id)
for style_name in PARSED_STYLES:
# 1. Ищем существующий стиль или по названию или по алиасам
existing = TbMusicStyle.objects.filter(
Q(s_style_name__iexact=style_name) |
Q(j_style_synonyms__contains=style_name)
).first()
if existing:
# ✅ МАТЧИНГ СРАБОТАЛ → пишем сразу в БД
if style_name not in existing.j_style_synonyms:
existing.j_style_synonyms.append(style_name) # Запомнили синоним!
existing.save()
else:
# ❌ НОВЫЙ СТИЛЬ → в Redis очередь на одобрение
redis.lpush('tasks:pending', {...})
# В метаданных источника сохраняем прогресс парсинга
source.j_source_metadata['last_parsed_line'] = current_row_number
source.j_source_metadata['total_lines'] = total_rows
source.j_source_metadata['parsed_at'] = timezone.now().isoformat()
source.save()
```
---
## АДМИНКА (упрощённо)
```python
class PendingTaskAdmin(admin.ModelAdmin):
# Не наследуем от ModelAdmin (нет моделей!)
# Вместо этого: читаем Redis, рендерим как таблицу
def changelist_view(self, request):
# Получаем из Redis
tasks = [json.loads(redis.get(key)) for key in redis.keys('pending:*')]
# Выводим нетипичную таблицу с кнопками: Одобрить, Мержить, Пропустить
```
---
## ФОНОВЫЙ ВОРКЕР (упрощённо)
```python
def process_approved_tasks():
while True:
task = redis.lpop('tasks:approved')
try:
if task['type'] == 'create':
# Пишем в product БД
TbMusicStyle.objects.create(title=task['title'], ...)
elif task['type'] == 'merge':
# Обновляем existing + добавляем синонимы
style = TbMusicStyle.objects.get(id=task['merge_with_id'])
style.j_style_synonyms.append(task['title'])
style.save()
# Успех → удалить из Redis
redis.delete(task['_redis_key'])
redis.lpush('tasks:completed', task)
except Exception as e:
redis.lpush('tasks:failed', {**task, 'error': str(e)})
```
---
## ТЕХНОЛОГИЧЕСКИЙ СТЕК
- **Redis** — очередь + кэш виртуальных объектов
- **Celery** или **APScheduler** — фоновый воркер для парсера и финального сохранения
- **Django Admin** — кастомная вкладка для управления очередью
- **PostgreSQL/SQLite** — product database (только одобренные данные)
---
## ВЕТКА: ПРЯМОЕ ПОПАДАНИЕ В БД (когда матчинг сработал)
Если парсер нашел алиас или группа уже в БД → **обходим очередь**, пишем сразу:
```python
def parse_source(source_id):
source = TbSource.objects.get(id=source_id)
for style_name in PARSED_STYLES:
# 1. Ищем существующий стиль или по названию или по алиасам
existing = TbMusicStyle.objects.filter(
Q(s_style_name__iexact=style_name) |
Q(j_style_synonyms__contains=style_name)
).first()
if existing:
# ✅ МАТЧИНГ СРАБОТАЛ → пишем сразу в БД
if style_name not in existing.j_style_synonyms:
existing.j_style_synonyms.append(style_name) # Запомнили синоним!
existing.save()
else:
# ❌ НОВЫЙ СТИЛЬ → в Redis очередь на одобрение
redis.lpush('tasks:pending', {...})
# В метаданных источника сохраняем прогресс парсинга
source.j_source_metadata['last_parsed_line'] = current_row_number
source.j_source_metadata['total_lines'] = total_rows
source.j_source_metadata['parsed_at'] = timezone.now().isoformat()
source.save()
```
---
## ИЗМЕНЕНИЕ ЦЕН (дополнительная ветка)
Когда уже существующий товар поменял цену → в отдельную очередь:
```
tasks:price_changes ← Отдельная очередь
└─ {type: 'price_update', offer_id: 42, old_price: 100, new_price: 120}
└─ {type: 'price_update', offer_id: 43, old_price: 200, new_price: 180}
```
**Обработка:**
- Фоновый воркер видит изменение цены
- Проверяет: изменилась ли существенно (> 5%)?
- Если да → может требоваться одобрение (+1 задание в Redis)
- Если нет → пишет сразу в БД (TbOfferHistory записывается автоматически)
---
## МЕТАДАННЫЕ ОТСЛЕЖИВАНИЯ ПРОГРЕССА
В каждой **TbSource** должно быть поле `j_source_metadata`:
```python
j_source_metadata = JSONField(default=dict, help_text='Отслеживание парсинга и синонимы')
# Структура:
{
"last_parsed_line": 4523, # Докуда добежал парсер (для resume)
"total_lines": 10000, # Всего строк в файле
"parsed_at": "2026-06-14T12:30:00",# Время последнего парсинга
"status": "in_progress", # in_progress | completed | failed
"error_message": null, # если failed, чтобы видно было почему
# Найденные синонимы (стили, артисты, которые auto-matched)
"matched_styles": {
"Rock": 45, # Стиль 45 найден под названием "Rock"
"rock": 45, # Вариант написания também сохранили
},
"matched_artists": {
"The Beatles": 12,
"Beatles, The": 12,
}
}
```
Если парсер упал на строке 4523 → перезапуск продолжится с 4524, а не с начала!
---
## МАСШТАБИРУЕМОСТЬ: REDIS В ПАМЯТИ vs PERSISTENCE
### Проблема: 10k+ задач при загрузке большого Excel
```
Excel с 10,000 позиций
├─ 10k артистов
├─ 5k стилей
├─ 2k лейблов
└─ 15k общих задач в очереди
```
Redis хранит в памяти по умолчанию ➜ контейнер перестартует ➜ всё теряется!
**Решение: RDB + AOF Persistence**
```yaml
# docker-compose.yml для Redis
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
command: >
redis-server
--appendonly yes
--appendfsync everysec
--save 900 1
--maxmemory 2gb
--maxmemory-policy allkeys-lru
```
- **RDB snapshots** — снимок каждые 15 минут
- **AOF log** — каждая команда лог записывается на диск
- **maxmemory-policy** — если память 2GB переполнится, удаляются старые задачи (но только pending, не approved!)
- **Persistence**: при рестарте контейнера Redis восстановит все задачи из AOF
---
## ПАРАЛЛЕЛЬНАЯ ОБРАБОТКА (масштабирование)
Если задач много → **несколько воркеров**, каждый обрабатывает свой тип:
```python
# Worker 1: Парсит (пишет в tasks:pending)
celery_app.send_task('parser.parse_source', args=[source_id])
# Worker 2: Обрабатывает стили (слушает tasks:approved тип='style')
@app.task
def process_style_task(task_data):
# обновить БД
# Worker 3: Обрабатывает артистов (слушает tasks:approved тип='artist')
@app.task
def process_artist_task(task_data):
# обновить БД
# Worker 4: Обрабатывает цены (слушает tasks:price_changes)
@app.task
def update_offer_price(task_data):
# обновить цену
```
Каждый воркер работает в отдельном потоке/процессе ➜ параллелизм!
---
## FLOW ПРИНЦИПИАЛЬНАЯ СХЕМА (обновлённая)
```
┌──────────────────────────────────┐
│ ПАРСЕР (Worker 1) │
│ Читает Excel/CSV/JSON │
└──────────────┬───────────────────┘
┌──────────────┴────────────┐
│ │
┌───────▼───────────┐ ┌─────────▼──────────┐
│ Матч сработал? │ │ Новые данные? │
│ (aliasing) │ │ (неизвестны) │
└────┬──────────────┘ └─────────┬──────────┘
│ Yes │ No
│ │
┌────────▼────────────┐ ┌────────▼────────────┐
│ СРАЗУ В БД │ │ tasks:pending │
│ + j_style_synonyms │ │ (очередь нужнофала) │
│ + metadata['matched'] │ (ждут одобрения) │
└────────┬────────────┘ └────────┬────────────┘
│ │
│ АДМИНКА ДЖАНГО
│ (виртуальная таблица)
│ │
│ ┌──────┴──────┐
│ │ ✅ ❌ 🔗 │
│ (Approve/Skip/Merge)
│ │ │
│ ┌───────▼─────┐ │
│ │ tasks: │ │
│ │ approved │ │
│ └───────┬─────┘ │
│ │ │
└──────────┬─────────┴─────────────┘
Worker 2,3,4...
┌──────▼──────┐
│ PRODUCT БД │
│ (валидные) │
└─────────────┘
```
---
## ДОПОЛНИТЕЛЬНЫЕ МЕТАДАННЫЕ ДЛЯ КАЖДОЙ ЗАДАЧИ
```python
# Каждая задача в Redis содержит:
task = {
'id': 'pending:style:123',
'type': 'style', # или 'artist', 'label', 'price_change'
'data': {'title': 'Rock', ...},
# Откуда пришла
'source_id': 42,
'source_name': 'Discogs API',
'parsed_line': 4523,
# Когда создана
'created_at': '2026-06-14T12:00:00',
'ttl_seconds': 86400, # жить 24 часа, потом удалить
# Статус обработки
'attempts': 0, # сколько раз пытались обработать
'last_error': null,
}
```
---
## ИТОГОВАЯ АРХИТЕКТУРА (расширенная)
```
ИСТОЧНИКИ (Excel/API)
↓ (15k записей)
┌──────────────────────────────────────────────────┐
│ REDIS ОЧЕРЕДИ (в памяти + AOF persistence) │
│ ├─ tasks:pending (очередь неодобренных) │
│ ├─ tasks:approved (одобренные) │
│ ├─ tasks:price_changes (изменения цен) │
│ ├─ tasks:matched (уже в БД + синонимы) │
│ ├─ tasks:completed (завершённые) │
│ └─ tasks:failed (ошибки) │
└──────────────────────────────────────────────────┘
↓ (видит админка) ↓ (видят воркеры)
┌──────────────────┐ ┌────────────────────┐
│ АДМИНКА ДЖАНГО │ │ WORKERS (Celery) │
│ (виртуальная │ │ ├─ parse_source │
│ таблица) │ │ ├─ process_style │
│ Действия: │ │ ├─ process_artist │
│ ok/del/edit/... │ │ └─ update_prices │
└──────────────────┘ └────────────────────┘
↓ ↓
└──────────┬─────────────┘
┌─────────────────────┐
│ PRODUCT БД │
│ (только валидные) │
│ + metadata progress │
│ + aliases │
└─────────────────────┘
```
+748
View File
@@ -0,0 +1,748 @@
# Организация справочника музыкальных стилей
# Состояние списка музыкальных стилей в Discogs (июль 2026)
_AOR, Aboriginal, Abstract, Acid, Acid House, Acid Jazz, Acid Rock, Acoustic, African, Afro-Cuban, Afro-Cuban Jazz,
Afrobeat, Afrobeat, Aguinaldo, Aleatoric, Alt-Pop, Alternative Metal, Amapiano, Ambient, Ambient House, Anarcho-Punk,
Anatolian Rock, Andalusian Classical, Andean Music, Anison, Antifolk, Appalachian Music, Arabic Pop, Arena Rock,
Art Rock, Art Song, Atmospheric Black, Metal, Atonal, Audiobook, Avant-garde Jazz, Axé, Azonto, Bachata, Baila,
Baião, Bakersfield Sound, Balearic, Ballad, Ballet, Ballet, Ballroom, Baltimore Club, Bambuco, Banda, Bangladeshi,
Classical, Barbershop, Baroque, Baroque Pop, Basque Music, Bass Music, Bass Music, Bassline, Batucada, Bayou Funk,
Beat, Beatbox, Beatdown, Beguine, Bel Canto, Bengali Music, Berlin-School, Bećarac, Bhangra, Big Band, Big Beat,
Bitpop, Black Metal, Blackgaze, Bleep, Bluegrass, Blues Rock, Bolero, Bollywood, Bomba, Bongo Flava, Boogaloo, Boogie,
Boogie Woogie, Boom Bap, Bop, Bossa Nova, Bossanova, Bounce, Bouncy Techno, Brass Band, Brass Instrument, Break-In,
Breakbeat, Breakcore, Breaks, Brega, Britcore, Britpop, Broken Beat, Brutal Death Metal, Bubblegum, Bubbling,
Bultrón, Byzantine, Cabaret, Caipira, Cajun, Calypso, Cambodian Classical, Candombe, Cantata, Cantopop, Cantorial,
Canzone Napoletana, Cape Jazz, Capriccio, Carimbó, Carnatic, Catalan Music, Celtic, Cha-Cha, Chacarera, Chamamé,
Chamber Music, Champeta, Chanson, Charanga, Chicago Blues, Chillwave, Chinese Classical, Chiptune, Choral, Choro,
Chutney, City Pop, Classic Rock, Classical, Classical Guitar, Cloud Rap, Cobla, Coldwave, Comedy, Comfy Synth, Compas,
Concert Band, Concert Film, Concerto, Concerto Grosso, Conjunto, Conscious, Contemporary, Contemporary Jazz,
Contemporary R&B, Cool Jazz, Copla, Corrido, Country, Country Blues, Country Rock, Cretan, Crossover Thrash,
Crunk, Crust, Cuatro, Cubano, Cumbia, Currulao, Cut-up/DJ, DJ Battle Tool, Dabke, Dance-pop, Dancehall, Dances,
Dangdut, Dansband, Danzon, Dark Ambient, Dark Electro, Dark Jazz, Darkwave, Death Metal, Deathcore, Deathrock,
Deconstructed Club, Deep House, Deep Techno, Delta Blues, Depressive Black, Metal, Descarga, Desert Blues, Dialogue,
Disco, Disco, Disco Polo, Divertimento, Dixieland, Donk, Doo Wop, Doo Wop, Doom Metal, Doomcore, Downtempo, Dream Pop,
Drill, Drone, Drum n Bass, Dub, Dub, Dub Poetry, Dub Techno, Dubstep, Dungeon Synth, Duranguense, EBM, Early,
East Coast Blues, Easy Listening, Education, Educational, Electric Blues, Electro, Electro, Electro House,
Electro Swing, Electroacoustic, Electroclash, Emo, Enka, Erotic, Ethereal, Ethno-pop, Etude, Euro House, Euro Trance,
Euro-Disco, Eurobeat, Eurodance, Europop, Exotica, Expressionist, Fado, Fantasia, Favela Funk, Field Recording, Filk,
Flamenco, Folk, Folk Metal, Folk Rock, Footwork, Formes Fixes, Forró, Free Funk, Free Improvisation, Free Jazz,
Freestyle, Freetekno, French House, Frevo, Funaná, Funeral Doom Metal, Funk, Funk Metal, Funkot, Fusion,
Future Bass, Future House, Future Jazz, Future Pop, Futurism, G-Funk, Gabber, Gagaku, Gaita, Galician Traditional,
Gamelan, Gangsta, Garage House, Garage Rock, Geet, Ghazal, Ghetto, Ghetto House, Ghettotech, Glam, Glitch, Glitch Hop,
Gnawa, Go-Go, Go-Go, Goa Trance, Goregrind, Gospel, Gospel, Goth Rock, Gothic Metal, Gqom, Grand Opera, Grime, Grime,
Grindcore, Griot, Groove Metal, Group Sounds, Grunge, Guaguancó, Guajira, Guaracha, Guarania, Guggenmusik, Gusle,
Gwo Ka, Gypsy Jazz, Għana, Halftime, Hands Up, Happy Hardcore, Hard Beat, Hard Bop, Hard House, Hard Rock,
Hard Techno, Hard Trance, Hardcore, Hardcore, Hardcore Hip-Hop, Hardstyle, Harmonica Blues, Harpsichord,
Harsh Noise Wall, Hawaiian, Health-Fitness, Heavy Metal, Hi NRG, Highlife, Hill Country Blues, Hillbilly, Hindustani,
Hip Hop, Hip-House, Hiplife, Hokkien Pop, Holiday, Honky Tonk, Honkyoku, Horror Rock, Horrorcore, House, Huayno,
Hyper Techno, Hyperpop, Hyphy, Hypnagogic pop, IDM, Illbient, Impressionist, Incidental Music, Indian Classical,
Indie Pop, Indie Rock, Indo-Pop, Industrial, Industrial, Industrial Metal, Instrumental, Interview, Italo House,
Italo-Disco, Italodance, Izvorna, J-Core, J-Rock, J-pop, Jangle Pop, Jazz-Funk, Jazz-Rock, Jazzdance, Jazzy Hip-Hop,
Jersey Club, Jibaro, Jiuta, Joropo, Jota, Jug Band, Juke, Jump Blues, Jumpstyle, Jungle, Junkanoo, K-Rock, K-pop,
Karaoke, Kaseko, Kaskawi, Kayōkyoku, Keroncong, Keyboard, Khaliji, Kizomba, Klasik, Klezmer, Kolo, Korean Court Music,
Krautrock, Kuduro, Kwaito, Lambada, Lao Music, Late Romantic, Latin, Latin Jazz, Latin Pop, Laïkó, Leftfield,
Lento Violento, Levenslied, Lied, Light Music, Liscio, Lo-Fi, Louisiana Blues, Lounge, Lovers Rock, Low Bap,
Lowercase, Luk Krung, Luk Thung, Lullaby, MPB, Madrigal, Magnificat, Makina, Maloya, Mambo, Mandopop, Manila Sound,
Marcha Carnavalesca, Marches, Mariachi, Marimba, Mass, Math Rock, Mathcore, Mbalax, Medical, Medieval,
Melodic Death Metal, Melodic Hardcore, Memphis Blues, Memphis Rap, Mento, Merengue, Metalcore, Miami Bass, Microhouse,
Microtonal, Midwest Emo, Military, Milonga, Min'yō, Minimal, Minimal Techno, Minimalism, Minneapolis Sound, Mizrahi,
Mo Lam, Mod, Modal, Modern, Modern Classical, Modern Electric, Blues, Monolog, Moombahton, Morna, Motet, Motswako,
Mouth Music, Movie Effects, Mugham, Musette, Music Hall, Music Video, Musical, Musique Concrète, Música Criolla, NDW,
Nagauta, Neo Soul, Neo Trance, Neo-Classical, Neo-Classical Metal, Neo-Romantic, Neofolk, Neofolk, Neopagan,
Nerdcore Techno, New Age, New Beat, New Jack Swing, New Wave, New Wave, Nhạc Vàng, Nintendocore, No Wave, Nocturne,
Noise, Noise, Noise Rock, Noisecore, Nordic, Norteño, Novelty, Nu Metal, Nu-Disco, Nueva Cancion, Nueva Trova,
Nursery Rhymes, Néo Kyma, Népzene, Occitan, Occult, Oi, Ojkača, Opera, Operetta, Oratorio, Orchestra, Organ,
Ottoman Classical, Overtone Singing, Overture, P.Funk, Pachanga, Pacific, Pagode, Parody, Parody, Pasodoble, Passion,
Percussion Ensemble, Persian Classical, Persian Pop, Philippine Classical, Phleng Phuea Chiwit, Phonk, Piano,
Piano Blues, Piedmont Blues, Piobaireachd, Pipe & Drum, Plainchant, Plena, Plunderphonics, Poetry, Political,
Polka, Pop Punk, Pop Rap, Pop Rock, Pornogrind, Porro, Post Bop, Post Rock, Post-Grunge, Post-Hardcore, Post-Metal,
Post-Modern, Post-Punk, Power Electronics, Power Metal, Power Pop, Power Violence, Prelude, Prog Rock, Program Music,
Progressive, Bluegrass, Progressive Breaks, Progressive House, Progressive Metal, Progressive Trance, Promotional,
Psy-Trance, Psychedelic, Psychedelic Rock, Psychobilly, Pub Rock, Public Broadcast, Public, Service Announcement,
Punk, Qawwali, Quechua, Radioplay, Ragga, Ragga HipHop, Ragtime, Ranchera, Rapso, Rara, Raï, Rebetiko, Reggae,
Reggae Gospel, Reggae-Pop, Reggaeton, Religious, Renaissance, Requiem, Rhapsody, Rhythm & Blues, Rhythm & Blues,
Rhythmic Noise, RnB/Swing, Rock & Roll, Rock Opera, Rockabilly, Rocksteady, Romani, Romantic, Roots Reggae, Rumba,
Rune Singing, Russian Pop, Ryūkōka, Rōkyoku, Salegy, Salsa, Samba, Samba-Canção, Sankyoku, Schlager, Schranz, Score,
Screamo, Screw, Sea Shanties, Sean-nós, Sephardic, Serenade, Seresta, Serial, Sermon, Sertanejo, Shaabi, Shibuya-Kei,
Shidaiqu, Shinkyoku, Shoegaze, Shomyo, Singeli, Singspiel, Ska, Ska, Skiffle, Skweee, Slowcore, Sludge Metal,
Smooth Jazz, Snap, Soca, Soft Rock, Sokyoku, Solo, Son, Son Montuno, Sonata, Sonero, Soukous, Soul, Soul-Jazz,
Sound Art, Sound Collage, Sound Poetry, Soundtrack, Southern Rock, Space Rock, Space-Age, Spaza, Special Effects,
Spectralism, Speech, Speed Garage, Speed Metal, Speedcore, Spiritual Jazz, Spirituals, Spoken Word, Sport, Sport,
Steel Band, Stoner Rock, Story, Stride, String Ensemble, String Instrument, Style Galant, Suite, Sunshine Pop,
Suomisaundi, Surf, Swamp Pop, Swing, Swingbeat, Symphonic Metal, Symphonic Rock, Symphony, Synth-pop, Synthpunk,
Synthwave, Sámi Music, Séga, Taarab, Tamburitza, Tamil Film Music, Tango, Te Deum, Tech House, Tech Trance,
Technical, Technical Death, Metal, Techno, Tejano, Texas Blues, Thai Classical, Theme, Theme With, Variations,
Therapy, Thrash, Thug Rap, Timba, Toasting, Tone Poem, Trallalero, Trance, Trap, Tribal, Tribal House, Trip Hop,
Trip Hop, Tropical House, Trova, Turntablism, Twelve-tone, Twist, UK Funky, UK Garage, UK Street Soul, Unblack Metal,
V-pop, Vallenato, Vaporwave, Vaudeville, Verismo, Video Game Music, Viking Metal, Villancicos, Vocal, Vocaloid,
Volksmusik, Waiata, Western Swing, Wind Ensemble, Witch House, Woodwind Instrument, Yemenite Jewish, Yoruba, Yé-Yé,
Zamba, Zarzuela, Zemer Ivri, Zhongguo Feng, Zouk, Zydeco, Éntekhno_
Всего: ~810
### Схема группировки для LPON для модели TbMusicStyle в синонимы
**Статус:** ✅ Загружено 22 основных стиля с 729 синонимами
| № | Категория | Синонимов | Примеры |
|-----|--------------------------------------------|-----------|-----------------------------------------------------------|
| 1 | Classic Rock | 14 | AOR, Acid Rock, Arena Rock, Hard Rock, Heavy Metal... |
| 2 | Alternative & Indie Rock | 19 | Alt-Pop, Grunge, Shoegaze, Indie Rock, Dream Pop... |
| 3 | Jazz | 21 | Big Band, Cool Jazz, Fusion, Bop, Dixieland... |
| 4 | Blues | 17 | Chicago Blues, Delta Blues, Jump Blues, Electric Blues... |
| 5 | Heavy Metal & Extreme Metal | 32 | Death Metal, Black Metal, Grindcore, Metalcore... |
| 6 | Punk & Hardcore | 19 | Punk, Post-Hardcore, Emo, Screamo, Crust.. |
| 7 | R&B & Hip Hop | 28 | Rap, Drill, Trap, Boom Bap, Memphis Rap... |
| 8 | Reggae & Dub | 14 | Dancehall, Roots Reggae, Ska, Rocksteady... |
| 9 | Folk, Country & Americana | 17 | Country, Bluegrass, Folk Rock, Honky Tonk... |
| 10 | Prog & Art Rock | 10 | Progressive Rock, Krautrock, Psychedelic... |
| 11 | Pop Music | 19 | Pop Rock, Bubblegum, Chanson, J-pop, K-pop... |
| 12 | Disco, Funk & Soul | 29 | Funk, Soul, Boogie, Hi NRG, Neo Soul... |
| 13 | Techno & House | 43 | House, Techno, Deep House, Minimal, Hardstyle... |
| 14 | Trance & Hardcore Electronic | 8 | Trance, Goa Trance, Psy-Trance, Hardcore... |
| 15 | Ambient, IDM & Down-tempo | 23 | Ambient, IDM, Drone, Trip Hop, Vaporwave... |
| 16 | Bass, Breakbeat & Drum n Bass | 35 | Drum n Bass, Jungle, Breakbeat, Dubstep... |
| 17 | Industrial, Noise & Avant-Garde Electronic | 11 | Industrial, Noise, EBM, Musique Concrète... |
| 18 | New Wave & Synth-Pop | 13 | Synth-pop, New Wave, Coldwave, Synthwave... |
| 19 | Classical & Academic | 72 | Classical, Baroque, Opera, Symphony, Chamber... |
| 20 | World, Ethnic & Traditional | 240 | Salsa, Tango, Flamenco, Bhangra, Gnawa... |
| 21 | Stage, Screen & Soundtracks | 13 | Soundtrack, Musical, Theme, Video Game Music... |
| 22 | Non-Music / Spoken Word / Miscellaneous | 32 | Audiobook, Spoken Word, Comedy, Interview... |
### Полный справочник синонимов
#### 1. Classic Rock (14 синонимов)
```
• AOR (Album Oriented Rock)
• Acid Rock
• Arena Rock
• Beat
• Hard Rock
• Heavy Metal
• Mod
• Pub Rock
• Rock & Roll
• Southern Rock
• Space Rock
• Surf
• Twist
```
#### 2. Alternative & Indie Rock (19 синонимов)
```
• Alt-Pop
• Alternative Rock
• Darkwave
• Dream Pop
• Goth Rock
• Grunge
• Hypnagogic Pop
• Indie Pop
• Indie Rock
• Jangle Pop
• Lo-Fi
• Math Rock
• Midwest Emo
• Post Rock
• Post-Grunge
• Shoegaze
• Slowcore
• Stoner Rock
```
#### 3. Jazz (21 синоним)
```
• Avant-garde Jazz
• Big Band
• Bop
• Cape Jazz
• Contemporary Jazz
• Cool Jazz
• Dark Jazz
• Dixieland
• Free Jazz
• Fusion
• Gypsy Jazz
• Hard Bop
• Jazz-Rock
• Jazzdance
• Modal
• Post Bop
• Smooth Jazz
• Spiritual Jazz
• Stride
• Swing
```
#### 4. Blues (17 синонимов)
```
• Blues Rock
• Chicago Blues
• Country Blues
• Delta Blues
• East Coast Blues
• Electric Blues
• Harmonica Blues
• Hill Country Blues
• Jug Band
• Jump Blues
• Louisiana Blues
• Memphis Blues
• Modern Electric Blues
• Piano Blues
• Piedmont Blues
• Texas Blues
```
#### 5. Heavy Metal & Extreme Metal (32 синонима)
```
• Alternative Metal
• Atmospheric Black Metal
• Black Metal
• Blackgaze
• Brutal Death Metal
• Crossover Thrash
• Death Metal
• Deathcore
• Depressive Black Metal
• Doom Metal
• Funeral Doom Metal
• Funk Metal
• Goregrind
• Gothic Metal
• Grindcore
• Groove Metal
• Industrial Metal
• Melodic Death Metal
• Melodic Hardcore
• Metalcore
• Neo-Classical Metal
• Nu Metal
• Post-Metal
• Power Metal
• Progressive Metal
• Sludge Metal
• Symphonic Metal
• Technical Death Metal
• Thrash
• Unblack Metal
• Viking Metal
```
#### 6. Punk & Hardcore (19 синонимов)
```
• Anarcho-Punk
• Antifolk
• Britpop
• Crust
• Emo
• Hardcore
• Oi
• Pop Punk
• Post-Hardcore
• Post Punk
• Power Pop
• Power Violence
• Psychobilly
• Punk
• Screamo
• Skiffle
```
#### 7. R&B & Hip Hop (28 синонимов)
```
• Amapiano
• Azonto
• Boom Bap
• Cloud Rap
• Conscious
• Contemporary R&B
• Crunk
• Drill
• Ghetto
• Hip Hop
• Hip-House
• Hiplife
• Horrorcore
• Hyperpop
• Hyphy
• Jazzy Hip-Hop
• Memphis Rap
• Motswako
• Nerdcore
• Phonk
• Pop Rap
• Ragga HipHop
• Rhythm & Blues
• Snap
• Spaza
• Thug Rap
• Trap
```
#### 8. Reggae & Dub (14 синонимов)
```
• Dancehall
• Dub
• Dub Poetry
• Lovers Rock
• Ragga
• Rapso
• Reggae Gospel
• Reggae-Pop
• Rocksteady
• Roots Reggae
• Ska
• Toasting
```
#### 9. Folk, Country & Americana (17 синонимов)
```
• Acoustic
• Appalachian Music
• Bakersfield Sound
• Bluegrass
• Country
• Country Rock
• Filk
• Folk
• Folk Metal
• Folk Rock
• Hillbilly
• Honky Tonk
• Neofolk
• Progressive Bluegrass
• Rockabilly
• Western Swing
```
#### 10. Prog & Art Rock (10 синонимов)
```
• Art Rock
• Berlin-School
• Group Sounds
• Krautrock
• Neo-Prog
• Prog Rock
• Psychedelic
• Psychedelic Rock
• Symphonic Rock
```
#### 11. Pop Music (19 синонимов)
```
• Ballad
• Baroque Pop
• Bubblegum
• Chanson
• Dance-pop
• Europop
• Highlife
• Holiday
• J-pop
• K-pop
• Kayōkyoku
• Mandopop
• Nursery Rhymes
• Pop Rock
• Russian Pop
• Sunshine Pop
• V-pop
• Vocal
• Yé-Yé
```
#### 12. Disco, Funk & Soul (29 синонимов)
```
• Acid Jazz
• Bayou Funk
• Boogie
• Boogie Woogie
• City Pop
• Disco
• Disco Polo
• Electro Swing
• Euro-Disco
• Free Funk
• Funk
• G-Funk
• Ghetto Tech
• Go-Go
• Hi NRG
• Italo-Disco
• Jazz-Funk
• Minneapolis Sound
• Motown
• Neo Soul
• New Jack Swing
• Nu-Disco
• P.Funk
• RnB/Swing
• Shibuya-Kei
• Soul
• Swingbeat
• UK Street Soul
```
#### 13. Techno & House (43 синонима)
```
• Acid House
• Ambient House
• Balearic
• Baltimore Club
• Bouncy Techno
• Deconstructed Club
• Deep House
• Deep Techno
• Donk
• Dub Techno
• Electro House
• Euro House
• Favela Funk
• French House
• Future House
• Garage House
• Ghetto House
• Hard House
• Hard Techno
• Hardstyle
• House
• Hyper Techno
• Italo House
• Italodance
• J-Core
• Jumpstyle
• Kuduro
• Lento Violento
• Makina
• Microhouse
• Minimal
• Minimal Techno
• Moombahton
• Nerdcore Techno
• New Beat
• Progressive House
• Schranz
• Speed Garage
• Tech House
• Techno
• Tribal House
• UK Funky
```
#### 14. Trance & Hardcore Electronic (8 синонимов)
```
• Euro Trance
• Goa Trance
• Hard Trance
• Neo Trance
• Progressive Trance
• Psy-Trance
• Trance
```
#### 15. Ambient, IDM & Down-tempo (23 синонима)
```
• Abstract
• Acid
• Ambient
• Chillwave
• Comfy Synth
• Dark Ambient
• Downtempo
• Drone
• Dungeon Synth
• Electro
• Glitch
• IDM
• Illbient
• Leftfield
• Lowercase
• New Age
• Plunderphonics
• Skweee
• Sound Art
• Trip Hop
• Vaporwave
• Witch House
```
#### 16. Bass, Breakbeat & Drum n Bass (35 синонимов)
```
• Bass Music
• Bassline
• Big Beat
• Bleep
• Bounce
• Breakbeat
• Breakcore
• Breaks
• Britcore
• Broken Beat
• Bubbling
• Club
• Cut-up/DJ
• DJ Battle Tool
• Drum n Bass
• Dubstep
• Footwork
• Freetekno
• Gabber
• Glitch Hop
• Grime
• Halftime
• Hands Up
• Happy Hardcore
• Hard Beat
• Jersey Club
• Juke
• Jungle
• Progressive Breaks
• Reggaeton
• Speedcore
• Supa
• Turntablism
• UK Garage
```
#### 17. Industrial, Noise & Avant-Garde Electronic (11 синонимов)
```
• EBM (Electronic Body Music)
• Electroacoustic
• Harsh Noise Wall
• Industrial
• Musique Concrète
• Noise
• Noise Rock
• Noisecore
• Power Electronics
• Rhythmic Noise
```
#### 18. New Wave & Synth-Pop (13 синонимов)
```
• Coldwave
• Electroclash
• Eurobeat
• Eurodance
• Future Pop
• NDW
• New Wave
• No Wave
• Néo Kyma
• Synth-pop
• Synthpunk
• Synthwave
```
#### 19. Classical & Academic (72 синонима)
```
• Andalusian Classical
• Baroque
• Bel Canto
• Byzantine
• Cantata
• Capriccio
• Chamber Music
• Choral
• Classical
• Classical Guitar
• Concert Band
• Concerto
• Concerto Grosso
• Contemporary
• Divertimento
• Early
• Expressionist
• Etude
• Fantasia
• Formes Fixes
• Futurism
• Harpsichord
• Impressionist
• Incidental Music
• Instrumental
• Keyboard
• Late Romantic
• Lied
• Light Music
• Madrigal
• Magnificat
• Mass
• Medieval
• Modern Classical
• Modern
• Motet
• Nocturne
• Opera
• Operetta
• Oratorio
• Orchestra
• Organ
• Overture
• Passion
• Percussion Ensemble
• Piano
• Pipe & Drum
• Plainchant
• Prelude
• Program Music
• Renaissance
• Requiem
• Rhapsody
• Romantic
• Serenade
• Serial
• Singspiel
• Solo
• Sonata
• Spectralism
• String Ensemble
• String Instrument
• Style Galant
• Suite
• Symphony
• Te Deum
• Theme With Variations
• Twelve-tone
• Verismo
• Wind Ensemble
• Woodwind Instrument
```
#### 20. World, Ethnic & Traditional (240 синонимов)
```
• Aboriginal, African, Afro-Cuban, Afrobeat
• Aguinaldo, Anatolian Rock, Andean Music, Arabic Pop
• Axé, Bachata, Baila, Baião, Ballet, Ballroom
• Bambuco, Banda, Bangladeshi Classical, Barbershop
• Basque Music, Batucada, Beguine, Bengali Music
• Bećarac, Bhangra, Bolero, Bollywood, Bomba
• Bongo Flava, Boogaloo, Bossa Nova, Bossanova
• Brass Band, Brass Instrument, Brega, Bultrón
• Caipira, Cajun, Calypso, Cambodian Classical
• Candombe, Cantopop, Cantorial, Canzone Napoletana
• Carimbó, Carnatic, Catalan Music, Celtic
• Cha-Cha, Chacarera, Chamamé, Champeta, Charanga
• Chinese Classical, Choro, Chutney, Cobla
• Compas, Conjunto, Copla, Corrido, Cretan
• Cuatro, Cubano, Cumbia, Currulao, Dabke
• Dangdut, Dansband, Danzon, Descarga, Desert Blues
• Duranguense, Ethno-pop, Exotica, Fado, Flamenco
• Forró, Freestyle, Frevo, Funaná, Funkot
• Gagaku, Gaita, Galician Traditional, Gamelan
• Geet, Ghazal, Gnawa, Gospel, Gqom, Griot
• Guaguancó, Guajira, Guaracha, Guarania, Guggenmusik
• Gusle, Gwo Ka, Għana, Hawaiian, Hindustani
• Hokkien Pop, Huayno, Indian Classical, Indo-Pop
• Izvorna, Jibaro, Jiuta, Joropo, Jota
• Junkanoo, Kaseko, Kaskawi, Keroncong, Khaliji
• Kizomba, Klasik, Klezmer, Kolo, Korean Court Music
• Kwaito, Lambada, Lao Music, Latin, Latin Jazz
• Latin Pop, Laïkó, Levenslied, Liscio, Lounge
• Luk Krung, Luk Thung, Lullaby, MPB, Maloya
• Mambo, Manila Sound, Marcha Carnavalesca, Marches
• Mariachi, Marimba, Mbalax, Mento, Merengue
• Miami Bass, Milonga, Min'yō, Mizrahi, Mo Lam
• Morna, Mugham, Musette, Música Criolla, Nagauta
• Neopagan, Nhạc Vàng, Nordic, Norteño, Nueva Cancion
• Nueva Trova, Népzene, Occitan, Ojkača, Ottoman Classical
• Overtone Singing, Pachanga, Pacific, Pagode, Pasodoble
• Persian Classical, Persian Pop, Philippine Classical
• Phleng Phuea Chiwit, Piobaireachd, Plena, Polka
• Porro, Qawwali, Quechua, Ranchera, Rara, Raï
• Rebetiko, Romani, Rōkyoku, Salegy, Salsa, Samba
• Samba-Canção, Sankyoku, Schlager, Sea Shanties
• Sean-nós, Sephardic, Seresta, Sertanejo, Shaabi
• Shidaiqu, Shinkyoku, Shomyo, Singeli, Soca
• Sokyoku, Son, Son Montuno, Sonero, Soukous
• Soul, Steel Band, Suomisaundi, Swamp Pop, Sámi Music
• Séga, Taarab, Tamburitza, Tamil Film Music, Tango
• Tejano, Thai Classical, Timba, Trallalero, Tribal
• Trova, Vallenato, Villancicos, Volksmusik, Waiata
• Yemenite Jewish, Yoruba, Zamba, Zarzuela
• Zemer Ivri, Zhongguo Feng, Zouk, Zydeco, Éntekhno
```
#### 21. Stage, Screen & Soundtracks (13 синонимов)
```
• Anison
• Cabaret
• Concert Film
• Movie Effects
• Music Hall
• Music Video
• Musical
• Score
• Soundtrack
• Theme
• Vaudeville
• Video Game Music
```
#### 22. Non-Music / Spoken Word / Miscellaneous (32 синонима)
```
• Audiobook
• Break-In
• Comedy
• Dialogue
• Education
• Educational
• Erotic
• Field Recording
• Health-Fitness
• Interview
• Karaoke
• Medical
• Military
• Monolog
• Novelty
• Parody
• Poetry
• Political
• Promotional
• Public Broadcast
• Public Service Announcement
• Radioplay
• Religious
• Sermon
• Special Effects
• Speech
• Spoken Word
• Sport
• Story
• Therapy
• Vocaloid
```
### Проверка целостности БД
**Все синонимы уникальны** — нет дубликатов внутри одной категории
**Нет пересечений** — синоним не может быть одновременно основным названием другой категории
**Полнота** — все ~810 стилей из Discogs распределены по 22 категориям
**Готовность к парсингу** — структура поддерживает будущую интеграцию с Discogs API
@@ -0,0 +1,195 @@
# Генерирование криптографической соли для OFFER_HASHIDS_SALT
## Для Development (локально)
Текущее значение в `.env`. Если база из dev будет перемещаться в продакшен, то соль не нужно будет заменять на уникальную для продакшена. А то все старые QR-коды перестанут работать.
Если все-таки нужно обновить соль, используйте Django custom command (см. ниже).
## Для Production (сервер)
**НИКОГДА** не используйте соль из примеров! Генерируйте новую для каждого окружения:
### Способ 1: Python (быстро)
```bash
python3 -c "import secrets; print(secrets.token_hex(16))"
```
Результат:
```
a7f3c82b9e1dEa6b5c8f2e3d0a9b4c7f
```
Скопируйте и обновите в `.env` на сервере:
```
OFFER_HASHIDS_SALT=a7f3c82b9e1dEa6b5c8f2e3d0a9b4c7f
```
### Способ 2: Linux/Mac (встроенный)
```bash
openssl rand -hex 16
```
### Способ 3: Более надежная соль (32 байта вместо 16)
```bash
python3 -c "import secrets; print(secrets.token_hex(32))"
```
Результат (64 символа, очень стойко):
```
26aebe8af9efe7c5f81c64cb18846318cc81a83eaf15aedb7f4b8a80990c9676
```
## Важные правила
1. **Уникальная для каждого окружения** (каждой реализации LPON под каждого сейлера)
- Dev ≠ Staging ≠ Production
- Разные соли = разные коды для одного ID
2. **Никогда не меняйте в production!**
- Если поменяете соль → старые коды перестанут работать
- Существующие QR-коды станут невалидными
- Существующие ID в базе не будут декодироваться
- **Если все же нужно изменить:** используйте custom command `regenerate_offer_codes` (см. ниже)
3. **Хранить в .env (не в репозитории)**
- `.env` в .gitignore ✓
- `.env.example` содержит шаблон ✓
## Проверка качества соли
```python
import secrets
# Хорошая соль (минимум 32 символа, hex-формат)
salt = secrets.token_hex(16) # ✓
salt = secrets.token_hex(32) # ✓✓ еще лучше
# Плохие соли
salt = "my-password" # ✗ слишком короткая
salt = "12345678" # ✗ предсказуемая
salt = "qwerty123" # ✗ слабая энтропия
```
## Параметры OFFER_HASHIDS_MIN_LENGTH
| Кол-во оферов | min_length | Пример | Примечание |
|---------------|------------|--------------|---------------------------------------|
| До 1 000 | 4 | `a1bC` | Слишком короткие, может быть коллизии |
| До 10 000 | 5 | `a1bCd` | Хорошо для небольших каталогов |
| До 100 000 | 6 | `a1bCdE` | **РЕКОМЕНДУЕМО** для начала |
| До 1 000 000 | 8 | `a1bCdEfG` | Для больших каталогов |
| Более 1M | 10 | `a1bCdEfGhI` | Очень большие каталоги |
**Текущее значение:** `OFFER_HASHIDS_MIN_LENGTH=6` (оптимально)
## Как увеличить при необходимости?
Если вырос каталог:
1. Обновите в `.env`:
```
OFFER_HASHIDS_MIN_LENGTH=8
```
2. Перезагрузите приложение
3. **Старые коды останутся валидными!** (hashids декодирует любой код независимо от min_length)
4. Новые офферы будут кодироваться с длиной 8
## Django Custom Command: `regenerate_offer_codes`
**Расположение:** `lpon_site/frontend/management/commands/regenerate_offer_codes.py`
Используется для проверки, восстановления и перегенерации s_offer_code офферов.
### Три режима работы
#### 1. Проверка кодов (режим `--check`)
Декодирует все коды обратно в ID и проверяет корректность:
```bash
# Быстрая проверка
cd lpon_site && poetry run python manage.py regenerate_offer_codes --check
# С подробным выводом
cd lpon_site && poetry run python manage.py regenerate_offer_codes --check --verbose
```
#### 2. Исправление некорректных кодов (режим `--fix-broken`)
Обновляет только коды, которые не декодируются правильно:
```bash
# Пробный запуск (ничего не сохранит)
cd lpon_site && poetry run python manage.py regenerate_offer_codes --fix-broken --dry-run
# Реальное исправление
cd lpon_site && poetry run python manage.py regenerate_offer_codes --fix-broken
```
**Использование:** Когда некоторые коды повреждены или закодированы неправильно (например, после сбоя БД).
#### 3. Полное обновление всех кодов (по умолчанию)
Перегенерирует ВСЕ коды на основе текущего OFFER_HASHIDS_SALT:
```bash
# Пробный запуск
cd lpon_site && poetry run python manage.py regenerate_offer_codes --dry-run
# Реальное обновление
cd lpon_site && poetry run python manage.py regenerate_offer_codes
```
**ВНИМАНИЕ:** Используется только при смене OFFER_HASHIDS_SALT на production!
### Примеры использования
**Сценарий 1: Проверка целостности после сбоя**
```bash
# Сначала проверяем что сломалось
cd lpon_site && poetry run python manage.py regenerate_offer_codes --check
# Если есть некорректные коды - исправляем
cd lpon_site && poetry run python manage.py regenerate_offer_codes --fix-broken
```
**Сценарий 2: Миграция на новый сервер с новой солью**
```bash
# 1. Обновляем .env с новой солью
OFFER_HASHIDS_SALT=новая_соль_из_secrets
# 2. Перегенерируем все коды (пробный запуск сначала)
cd lpon_site && poetry run python manage.py regenerate_offer_codes --dry-run
# 3. Если хорошо - реальное обновление
cd lpon_site && poetry run python manage.py regenerate_offer_codes
# 4. Проверяем что все работает
cd lpon_site && poetry run python manage.py regenerate_offer_codes --check
```
### Опции команды
```
--check Проверить корректность всех кодов (декодировать обратно в id)
--fix-broken Обновить только коды которые не декодируются правильно
--dry-run Показать что будет изменено, но не сохранять
--verbose Показывать подробный прогресс для каждого оффера
```
Использование:
```bash
# Проверка
python manage.py regenerate_offer_codes --check
# Исправление
python manage.py regenerate_offer_codes --fix-broken
# Полное обновление (при смене соли)
python manage.py regenerate_offer_codes
```
@@ -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 для парсера
- [ ] Написать тесты для валидации и парсинга
+414
View File
@@ -0,0 +1,414 @@
# Архитектура парсера и валидации данных
**Дата:** 2026-07-15
**Последний обновленно:** 2026-07-15
**Статус:** В разработке ✏️
## Обзор проблемы
Система должна поддерживать импорт данных из различных источников:
- Excel/CSV файлы от продавцов и издателей
- Парсинг URL-страниц с каталогами
- Ручной ввод данных в админке
- Будущие API интеграции
**Основной вызов:** Валидация и обнаружение конфликтов (дубликаты, совпадения в синонимах) требует **пользовательского участия** для принятия решений. Нельзя просто блокировать или автоматически удалять/объединять данные.
## Текущее состояние (MVP)
### Модели и структура
```
┌─ TbSource (Excel, CSV, URL) ──┐
│ s_source_name │
│ l_source_type (excel/csv/url)│
│ k_source_to_seller ──────────┼──► TbSeller
│ source_file (FilerFileField) │
│ s_source_url │
│ j_source_metadata (struktura)│ (metadata описывает столбцы, вкладки и т.д.)
└─────────────────────────────────┘
TbOffer ◄──┐
TbSource ──┴─► TbItem ──────► TbArticle (текст, SEO, slug, картинка)
└──► TbLabel, TbArtist, TbMusicStyle
(все через TbArticle)
```
### Валидация (текущая реализация)
**Файл:** `lpon_site/frontend/utils_validators.py`
**Функция:** `validate_for_duplicates()`
- Проверяет основное поле (s_label, s_artist и т.д.)
- Проверяет синонимы в j_*_metadata[SYN_EN]
- Возвращает список найденных дубликатов с типом совпадения
**Типы совпадений:**
1. **EXACT_MATCH** — точное совпадение основного поля (s_label == s_label)
2. **FIND_IN_SYNONYM** — основное поле текущей записи найдено в синонимах другой
3. **EXACT_SYNONYM_MATCH** — синонимы текущей записи совпадают с синонимами другой
**Где вызывается:**
-В админке: `admin.py` (переопределение `clean()` формы) → показывает ошибку пользователю
-В моделях: `save()` методы → выбрасывают `ValidationError`
-В парсере: **НЕТ** (парсер еще не создан)
### Конфликты синонимов (TODO)
**Сценарий 1: FIND_IN_SYNONYM**
```python
# В БД уже есть
Artist(pk=1, s_artist="Beatles", j_artist_metadata={"SYN_EN": ["The Beatles", "Beatles"]})
# Парсер хочет создать
Artist(pk=None, s_artist="The Beatles", j_artist_metadata={"SYN_EN": []})
# Валидатор: основное поле "The Beatles" найдено в синонимах существующей записи!
# Текущее поведение: ValidationError (блокировка)
# TODO: Отправить в очередь, ждать решения админа
```
**Сценарий 2: EXACT_SYNONYM_MATCH**
```python
# В БД
Label(pk=1, s_label="Sony", j_label_metadata={"SYN_EN": ["Sony Records", "Sony Music"]})
# Парсер хочет
Label(pk=None, s_label="Sony Music", j_label_metadata={"SYN_EN": ["Sony Records", "Sony Music"]})
# Валидатор: синонимы совпадают!
# Текущее поведение: ValidationError
# TODO: Отправить в очередь, ждать решения админа
```
## Архитектура парсера (требуемая)
### Компоненты
```
┌──────────────────────────────────────────────────────────────┐
│ DJANGO ADMIN │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Приложение "Parser" (новое приложение) │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ Очередь валидации (ValidatorQueue model) │ │ │
│ │ │ Содержит: состояние экземпляра, ошибки, решение... │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ Admin list view + inline actions │ │ │
│ │ │ - Просмотр конфликта (дубликаты) │ │ │
│ │ │ - Кнопка "Создать новую запись" │ │ │
│ │ │ - Кнопка "Объединить/обновить синонимы" │ │ │
│ │ │ - Кнопка "Пропустить" │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ БРОКЕР ОЧЕРЕДИ (Redis) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ parser:validation:pending (ZSET) │ │
│ │ parser:validation:{queue_id}:data (STRING/JSON) │ │
│ │ parser:validation:{queue_id}:duplicates (JSON) │ │
│ │ parser:validation:{queue_id}:solution (STRING) │ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ ПАРСЕР (Celery Task) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ parse_excel() / parse_csv() / parse_url() │ │
│ │ │ │
│ │ Логика: │ │
│ │ 1. Читать источник (файл/URL) │ │
│ │ 2. Нормализовать данные │ │
│ │ 3. Валидировать через validate_for_duplicates() │ │
│ │ 4. Если конфликт → создать запись в ValidatorQueue │ │
│ │ 5. Если OK → создать модель (TbLabel, TbArtist и т.д.)│ │
│ │ 6. Отправить уведомление админу (Redis/сигнал) │ │
│ │ 7. Продолжить обработку следующей строки │ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ СИГНАЛЫ И УВЕДОМЛЕНИЯ (Django signals) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ post_validate_conflict → Redis уведомление админу │ │
│ │ post_admin_decision → Обновление очереди │ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
```
### Таблица ValidatorQueue (новая модель)
```python
class ValidatorQueue(models.Model):
"""
Очередь конфликтов, требующих решения админа.
"""
class Status(TextChoices):
PENDING = 'pending' # Ждёт решения админа
APPROVED = 'approved' # Админ одобрил - выполнить действие
REJECTED = 'rejected' # Админ отклонил
MERGED = 'merged' # Данные объединены с существующей записью
SKIPPED = 'skipped' # Заметка: пропущено
class ConflictType(TextChoices):
FIND_IN_SYNONYM = 'find_in_synonym' # Основное поле в синонимах
EXACT_SYNONYM_MATCH = 'exact_synonym_match' # Синонимы совпадают
OTHER = 'other'
# Основные поля
id = BigAutoField(primary_key=True)
k_source = ForeignKey(TbSource, ...) # Откуда пришли данные
l_conflict_type = CharField(choices=ConflictType)
# Данные для конфликта
l_model_type = CharField() # Какая модель (TbLabel, TbArtist и т.д.)
model_pk = IntegerField(null=True) # PK существующей записи в БД
j_incoming_data = JSONField() # Данные от парсера (весь объект)
j_duplicates = JSONField() # Результат validate_for_duplicates()
# Решение админа
l_status = CharField(choices=Status, default=Status.PENDING)
s_admin_decision = CharField() # 'create_new' / 'merge' / 'skip'
s_admin_notes = TextField() # Комментарий админа
# Сервисные поля
t_created = DateTimeField(auto_now_add=True)
t_resolved = DateTimeField(null=True)
k_resolved_by = ForeignKey(User, null=True) # Какой админ принял решение
class Meta:
verbose_name = 'Конфликт валидации'
ordering = ['-t_created']
indexes = [
Index(fields=['l_status', '-t_created']),
Index(fields=['k_source', 'l_status']),
]
```
## Процесс работы
### Фаза 1: Импорт данных (парсер)
```python
# tasks.py (Celery)
@celery.task
def parse_and_validate_excel(source_id: int):
"""
1. Читает Excel из TbSource
2. Парсит строки
3. Валидирует каждую запись
4. Создаёт или отправляет в очередь
"""
source = TbSource.objects.get(pk=source_id)
for row in read_excel(source.source_file):
try:
# Нормализуем данные
artist_data = {
's_artist': row['Artist Name'],
'j_artist_metadata': {'SYN_EN': [row.get('Aliases', '')]}
}
# Валидируем
duplicates = validate_for_duplicates(
model_class=TbArtist,
instance_pk=None,
main_field_value=artist_data['s_artist'],
metadata_dict=artist_data['j_artist_metadata'],
main_field_name='s_artist',
metadata_field_name='j_artist_metadata',
)
if duplicates:
# КОНФЛИКТ! Отправляем в очередь
queue_item = ValidatorQueue.objects.create(
k_source=source,
l_model_type='TbArtist',
l_conflict_type=duplicates[0][VALIDATE_KEY__MATCH_TYPE],
j_incoming_data=artist_data,
j_duplicates=duplicates,
)
# Отправляем сигнал (Redis уведомление админу)
send_to_redis_queue('parser:conflicts:new', {
'queue_id': queue_item.id,
'model': 'TbArtist',
'conflict': duplicates[0]['match_type'],
'data': artist_data,
})
else:
# Нет конфликта - создаём запись
artist = TbArtist.objects.create(**artist_data)
logger.info(f"Artist created: {artist.id}")
except Exception as e:
logger.error(f"Parse error in row {row}: {e}")
continue
```
### Фаза 2: Админка (приложение Parser)
```python
# admin.py (новое приложение parser)
@admin.register(ValidatorQueue)
class ValidatorQueueAdmin(admin.ModelAdmin):
"""
Админка для работы с очередью конфликтов.
"""
list_display = [
'id', 'l_model_type', 'l_conflict_type', 'l_status',
'source_link', 'duplicates_summary', 't_created'
]
list_filter = ['l_status', 'l_conflict_type', 'l_model_type', 't_created']
readonly_fields = ['j_incoming_data', 'j_duplicates', 'k_source']
fieldsets = (
('Конфликт', {
'fields': ['l_model_type', 'l_conflict_type', 'k_source', 'model_pk']
}),
('Входящие данные', {
'fields': ['j_incoming_data']
}),
('Найденные дубликаты', {
'fields': ['j_duplicates']
}),
('Решение админа', {
'fields': ['s_admin_decision', 's_admin_notes', 'l_status']
}),
)
actions = ['action_create_new', 'action_merge_synonyms', 'action_skip']
def action_create_new(self, request, queryset):
"""Админ решил: создать новую запись, игнорируя дубликаты"""
for item in queryset:
model_class = get_model_class(item.l_model_type)
instance = model_class(**item.j_incoming_data)
instance.save(skip_validation=True) # Пропускаем валидацию
item.l_status = ValidatorQueue.Status.APPROVED
item.s_admin_decision = 'create_new'
item.k_resolved_by = request.user
item.t_resolved = now()
item.save()
```
### Фаза 3: Обработка решения админа
```python
# signals.py (новое приложение parser)
@receiver(post_save, sender=ValidatorQueue)
def on_admin_decision(sender, instance, created=False, **kwargs):
"""
Обработать решение админа и применить действие.
"""
if created or instance.l_status != ValidatorQueue.Status.APPROVED:
return
if instance.s_admin_decision == 'create_new':
# Админ одобрил создание новой записи
model_class = get_model_class(instance.l_model_type)
model_instance = model_class(**instance.j_incoming_data)
model_instance.save(skip_validation=True)
elif instance.s_admin_decision == 'merge':
# Админ решил объединить (добавить синонимы, и т.д.)
existing_pk = instance.model_pk
incoming_data = instance.j_incoming_data
model_class = get_model_class(instance.l_model_type)
existing = model_class.objects.get(pk=existing_pk)
# Добавляем синонимы из входящих данных
existing_meta = existing.j_*_metadata or {}
existing_synonyms = existing_meta.get(KEY_SYNONYM_EN, [])
incoming_synonyms = incoming_data.get('j_*_metadata', {}).get(KEY_SYNONYM_EN, [])
# Объединяем и сохраняем
existing_meta[KEY_SYNONYM_EN] = list(set(existing_synonyms + incoming_synonyms))
existing.j_*_metadata = existing_meta
existing.save(skip_validation=True)
```
## Файлы для создания
### 1. Новое приложение `parser`
```
lpon_site/parser/
├── __init__.py
├── models.py # ValidatorQueue
├── admin.py # ValidatorQueueAdmin + actions
├── signals.py # Обработка решений админа
├── tasks.py # Celery tasks для парсинга
├── apps.py
└── migrations/
```
### 2. Изменения в existing коде
**settings.py:**
```python
INSTALLED_APPS = [
...,
'parser', # Новое приложение
]
# Конфигурация Celery (если еще нет)
CELERY_BROKER_URL = 'redis://localhost:6379/0'
CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'
```
**frontend/models.py:**
```python
# В методе save() моделей добавить параметр
def save(self, *args, skip_validation=False, **kwargs):
if not skip_validation:
# Стандартная валидация
validate_and_raise_for_duplicates(...)
super().save(*args, **kwargs)
```
**frontend/utils.py:**
```python
# Добавить функцию отправки в Redis
def send_to_redis_queue(queue_name: str, data: dict):
"""Отправить уведомление в Redis"""
import redis
import json
r = redis.Redis(host='localhost', port=6379, db=0)
r.lpush(queue_name, json.dumps({
'timestamp': datetime.now().isoformat(),
**data
}))
```
## TODO задачи
- [ ] Создать приложение `parser` с моделью `ValidatorQueue`
- [ ] Реализовать админку `ValidatorQueueAdmin` с actions
- [ ] Написать Celery tasks для парсинга Excel/CSV
- [ ] Добавить обработку решений админа через signals
- [ ] Интегрировать Redis уведомления админу
- [ ] Добавить параметр `skip_validation` в save() методы
- [ ] Написать тесты для парсера и валидации
- [ ] Документация для парсера (как использовать)
## Примечания
1. **Redis vs БД для очереди?** → Используем БД (ValidatorQueue модель) как основное хранилище, Redis только для уведомлений админу в реальном времени
2. **Async валидация?** → Парсер работает в Celery, валидация синхронна внутри задачи
3. **Миграция истории?** → Поле `t_history_created` уже `editable=True` в TbOfferHistory, так что админ может вручную добавлять исторические данные
4. **Конфликты в админке?**`ValidatorQueue` блокирует парсер от создания дубликатов. Админ сам решает что делать.
## Ссылки на существующий код
- Валидаторы: `lpon_site/frontend/utils_validators.py`
- Модели: `lpon_site/frontend/models.py` (методы save() для TbArtist, TbItem, TbLabel, TbSeller, TbMusicStyle)
- TODO про Redis: `lpon_site/frontend/utils.py` (в функции `create_or_get_related_article()`)
- TODO про историю: `lpon_site/frontend/models.py``TbOffer.save()`)