ua5web_publisherpublisher_folders, publisher_messages, publisher_migration_logpublisherStorageAdapter.js + publisherRepository.jsmysql2/promise pool, connection.jsPUBLISHER_STORAGE_MODE=mysqlЩо таке Repository? — це файл, який спілкується з MySQL. Він знає як зберігати і читати дані з бази. Наприклад, nexusRepository.js вміє зберігати профілі.
Що таке Storage Adapter? — це перемикач. Якщо ENV = json, він читає файл. Якщо ENV = mysql, він йде в базу. Код модуля не знає різниці.
Чому Nexus найскладніший? — один профіль зберігає вкладені обʼєкти: static_data (незмінне), current_state (поточне), timeline (історія подій). Все це треба розкласти по 4 таблицях.
Як тестувати? — встановити NEXUS_STORAGE_MODE=mysql в .env, рестартнути PM2, відкрити адмінку і перевірити що профілі показуються як раніше.
Що таке сесії? — одне завдання, яке виконується групою. Має куратора, учасників, бали за участь. Кожна сесія привʼязана до картки завдання.
Навіщо 5 таблиць? — картки (шаблони завдань), сесії (конкретні виконання), учасники (хто брав участь), коригування балів (бонуси/штрафи), статистика (загальні підсумки).
Як працює міграція? — скрипт читає JSON файли, перетворює дані у формат таблиць (розгортає вкладені обʼєкти), і вставляє рядок за рядком у MySQL.
Що таке daily reset? — щоденне обнулення лічильників активності опівночі. Дані за вчора зберігаються в історію, а сьогоднішні починаються з нуля.
Що таке стріки? — послідовні дні активності без пропусків. Якщо пропустив день — стрік обнуляється. Найдовший стрік зберігається окремо.
Навіщо окремі таблиці для levels/categories? — це довідники (рівні складності, категорії досягнень). Вони рідко змінюються і використовуються як FK для основної таблиці досягнень.
Чому це найпростіше? — плоскі структури без вкладеності. Кожен запис — це простий рядок з кількома полями. Мало даних, прості SELECT/INSERT запити.
Два модулі в одній фазі? — обидва маленькі (заявки і список учасників), тому обʼєднані. Можна зробити обидва за один підхід.
Що таке cleanup? — видалення старого JSON fallback коду з Publisher (він вже повністю на MySQL). Також видалення тимчасових файлів міграції.
Що таке rollback? — якщо щось зламалось після переключення на MySQL, просто повертаємо *_STORAGE_MODE=json в .env і рестартимо PM2. JSON файли залишаються як бекап.
| Колонка | Тип | Примітки |
|---|---|---|
| id | INT AUTO_INCREMENT | PRIMARY KEY |
| user_id | VARCHAR(20) | UNIQUE NOT NULL |
| thread_id | VARCHAR(20) | |
| thread_url | VARCHAR(255) | |
| created_at | DATETIME | CURRENT_TIMESTAMP |
| updated_at | DATETIME | ON UPDATE |
| Колонка | Тип | Примітки |
|---|---|---|
| id | INT AUTO_INCREMENT | PRIMARY KEY |
| user_id | VARCHAR(20) | UNIQUE, FK → nexus_profiles |
| discord_username | VARCHAR(100) | |
| discord_global_name | VARCHAR(100) | |
| first_joined_at | DATETIME | |
| account_created_at | DATETIME | |
| invite_code | VARCHAR(50) | |
| invited_by_id | VARCHAR(20) | |
| member_number | INT | |
| is_legacy_user | BOOLEAN | DEFAULT FALSE |
| legacy_note | TEXT |
| Колонка | Тип | Примітки |
|---|---|---|
| id | INT AUTO_INCREMENT | PRIMARY KEY |
| user_id | VARCHAR(20) | UNIQUE, FK → nexus_profiles |
| server_nickname | VARCHAR(255) | |
| avatar_url | VARCHAR(512) | |
| is_on_server | BOOLEAN | DEFAULT TRUE |
| is_banned | BOOLEAN | DEFAULT FALSE |
| current_tag | VARCHAR(50) | |
| current_rank | VARCHAR(50) | INDEX |
| reputation | INT | DEFAULT 0 |
| points | INT | DEFAULT 0, INDEX |
| tasks_completed | INT | DEFAULT 0 |
| tasks_failed | INT | DEFAULT 0 |
| voice_time_minutes | INT | DEFAULT 0 |
| messages_count | INT | DEFAULT 0 |
| last_active | DATETIME | INDEX |
| warnings_total | INT | DEFAULT 0 |
| timeouts_total | INT | DEFAULT 0 |
| kicks_received | INT | DEFAULT 0 |
| bans_received | INT | DEFAULT 0 |
| achievements | JSON | Масив ID |
| roles | JSON | Масив ID |
| rank_history | JSON | Масив обʼєктів |
| Колонка | Тип | Примітки |
|---|---|---|
| id | INT AUTO_INCREMENT | PRIMARY KEY |
| event_id | VARCHAR(50) | UNIQUE |
| user_id | VARCHAR(20) | FK → nexus_profiles, INDEX |
| type | VARCHAR(50) | INDEX |
| timestamp | DATETIME | INDEX |
| message_id | VARCHAR(20) | |
| data | JSON | Payload події |
| Колонка | Тип | Примітки |
|---|---|---|
| id | VARCHAR(20) | PRIMARY KEY |
| name | VARCHAR(255) | NOT NULL |
| description | TEXT | |
| objective | TEXT | |
| type | VARCHAR(20) | DEFAULT 'neutral' |
| category | ENUM('easy','medium','hard') | INDEX |
| level | INT | DEFAULT 1 |
| points | INT | DEFAULT 0 |
| reward_money | INT | DEFAULT 0 |
| min_participants | INT | DEFAULT 2 |
| max_participants | INT | DEFAULT 8 |
| estimated_duration | INT | Хвилини |
| is_active | BOOLEAN | DEFAULT TRUE, INDEX |
| Колонка | Тип | Примітки |
|---|---|---|
| id | INT AUTO_INCREMENT | PRIMARY KEY |
| session_id | VARCHAR(50) | UNIQUE |
| card_id | VARCHAR(20) | FK → executor_cards |
| task_name | VARCHAR(255) | |
| curator_id | VARCHAR(20) | INDEX |
| status | ENUM('active','completed','cancelled') | INDEX |
| total_points | INT | DEFAULT 0 |
| completed_at | DATETIME | INDEX |
| Колонка | Тип | Примітки |
|---|---|---|
| id | INT AUTO_INCREMENT | PRIMARY KEY |
| session_id | VARCHAR(50) | FK → executor_sessions |
| user_id | VARCHAR(20) | INDEX |
| is_curator | BOOLEAN | DEFAULT FALSE |
| confirmed | BOOLEAN | DEFAULT FALSE |
| earned_points | INT | DEFAULT 0 |
| final_points | INT | DEFAULT 0 |
| Колонка | Тип | Примітки |
|---|---|---|
| id | INT AUTO_INCREMENT | PRIMARY KEY |
| user_id | VARCHAR(20) | UNIQUE |
| total_points | INT | DEFAULT 0, INDEX |
| tasks_completed | INT | DEFAULT 0 |
| mvp_count | INT | DEFAULT 0 |
| penalties_received | INT | DEFAULT 0 |
| bans_received | INT | DEFAULT 0 |
| tasks_by_card | JSON | Статистика по картках |
| Колонка | Тип | Примітки |
|---|---|---|
| id | INT | PRIMARY KEY |
| name | VARCHAR(255) | NOT NULL |
| description | TEXT | |
| icon | VARCHAR(50) | |
| level | INT | FK → achievement_levels |
| category | VARCHAR(50) | FK → achievement_categories |
| points | INT | DEFAULT 0 |
| conditions | JSON | Умови розблокування |
| reward | JSON | Необовʼязкове |
| card_config | JSON | Візуальні налаштування |
| Колонка | Тип | Примітки |
|---|---|---|
| id | INT AUTO_INCREMENT | PRIMARY KEY |
| user_id | VARCHAR(20) | UNIQUE(user_id, date) |
| date | DATE | INDEX |
| voice_minutes | INT | DEFAULT 0 |
| messages | INT | DEFAULT 0 |
| reactions_given | INT | DEFAULT 0 |
| tasks_completed | INT | DEFAULT 0 |
| total_points | INT | DEFAULT 0 |
| is_active | BOOLEAN | DEFAULT FALSE |
| Колонка | Тип | Примітки |
|---|---|---|
| id | INT AUTO_INCREMENT | PRIMARY KEY |
| user_id | VARCHAR(20) | UNIQUE |
| current_streak | INT | DEFAULT 0, INDEX |
| longest_streak | INT | DEFAULT 0 |
| last_active_date | DATE | |
| streak_started_at | DATE |
| Колонка | Тип | Примітки |
|---|---|---|
| id | VARCHAR(100) | PRIMARY KEY |
| user_id | VARCHAR(20) | INDEX |
| username | VARCHAR(100) | |
| display_name | VARCHAR(200) | |
| form_data | JSON | NOT NULL |
| status | ENUM('pending','accepted','rejected') | INDEX |
| submitted_at | DATETIME | INDEX |
| reviewed_at | DATETIME | |
| reviewed_by | VARCHAR(20) | |
| reject_reason | TEXT |
| Колонка | Тип | Примітки |
|---|---|---|
| id | INT AUTO_INCREMENT | PRIMARY KEY |
| number | INT | NOT NULL |
| discord_id | VARCHAR(20) | UNIQUE |
| username | VARCHAR(100) | |
| joined_at | DATETIME | NOT NULL |
| source | VARCHAR(50) | DEFAULT 'real' |
| Колонка | Тип | Примітки |
|---|---|---|
| id | INT AUTO_INCREMENT | PRIMARY KEY |
| week_id | VARCHAR(20) | UNIQUE |
| week_start | DATETIME | |
| week_end | DATETIME | |
| income | JSON | |
| deductions | JSON | |
| distributable | INT | DEFAULT 0 |
| directions_allocation | JSON | |
| published | BOOLEAN | DEFAULT FALSE |
| created_at | DATETIME | |
| updated_at | DATETIME |
| Колонка | Тип | Примітки |
|---|---|---|
| id | INT AUTO_INCREMENT | PRIMARY KEY |
| week_id | VARCHAR(20) | INDEX |
| data | JSON | Повний знімок тижня |
| completed_at | DATETIME |
| Колонка | Тип | Примітки |
|---|---|---|
| id | INT AUTO_INCREMENT | PRIMARY KEY |
| action | VARCHAR(100) | NOT NULL |
| user_id | VARCHAR(20) | INDEX |
| details | JSON | |
| timestamp | DATETIME | INDEX |
Кожен модуль отримує адаптер, який зчитує *_STORAGE_MODE з ENV. Якщо json — читає/пише JSON файли. Якщо mysql — делегує в репозиторій.
Чистий MySQL шар. Маппери fromDB() / toDB() конвертують camelCase ↔ snake_case. Використовує INSERT ... ON DUPLICATE KEY UPDATE для upsert.
Єдиний mysql2/promise пул в connection.js. Спільний для всіх репозиторіїв. Авто-міграція через ensureSchema() при старті.
Одноразовий скрипт для кожного модуля: читає JSON, трансформує дані, вставляє в MySQL. Логує в *_migration_log. Запускається вручну.
Кожен модуль керується незалежно:
PUBLISHER_STORAGE_MODE=mysql
NEXUS_STORAGE_MODE=json
EXECUTOR_STORAGE_MODE=json
CHALLENGER_STORAGE_MODE=json
JSON файли зберігаються як бекап. Переключити ENV назад на json і рестартнути — миттєвий відкат. Втрата даних неможлива під час переходу.