- spotify URL resolution was broken (500 NPE): preferPartnerApi + customTokenEndpoint depended on dead spotify-tokener (-32000) - remove tokener service from compose, remove dead SPOTIFY_* env from bot container (bot code never used them) - spotify metadata now resolves via standard client credentials, playback is mirrored to SoundCloud via scsearch providers (ISRC/QUERY)
628 lines
43 KiB
Markdown
628 lines
43 KiB
Markdown
# TODO — Loki Bot v2.1
|
||
|
||
## ✅ Что уже сделано
|
||
|
||
| # | Статус | Проблема | Изменения |
|
||
|---|--------|----------|-----------|
|
||
| C1–C15 | ✅ | Предыдущие раунды | См. git history |
|
||
| J21 | ✅ | Java 17 → 21 | `build.gradle`: source/target 21 |
|
||
| A05 | ✅ | EventDispatcher: CachedThreadPool → VirtualThreadPool | `Executors.newVirtualThreadPerTaskExecutor()` |
|
||
| A06 | ✅ | SwapCommand + RepTopCommand: `new Thread()` → `Thread.startVirtualThread()` | |
|
||
| A25 | ✅ | CommandExceptionHandler: instanceof pattern matching | |
|
||
| A26 | ✅ | ClearCommand: instanceof pattern matching | |
|
||
| A27 | ✅ | DatabaseMigration: instanceof pattern matching | |
|
||
| A28 | ✅ | InfoCommand: switch → arrow-switch | |
|
||
| A29 | ✅ | RateCommand: `"⭐".repeat(rating)` + `"☆".repeat()` | |
|
||
| A32 | ✅ | .collect(Collectors.toList()) → .toList() | SwapCommand, RepTopCommand, ChoiceOptionParser |
|
||
| A33 | ✅ | HelpCategories: Map.ofEntries() вместо HashMap + unmodifiable | |
|
||
| A02 | ✅ | RankCardGenerator: блокировка Event Thread | Исправлено (команды на virtual threads) |
|
||
| A10 | ✅ | RankCardGenerator: утечка InputStream | try-with-resources в loadFont + loadBackground |
|
||
| A11 | ✅ | RankCardGenerator: шрифты не кэшировались | ConcurrentHashMap cache |
|
||
| A12 | ✅ | RankCardGenerator: SVG-иконки не работали | Заменены на геометрические фигуры (Java2D) |
|
||
| A23 | ✅ | RankCardGenerator: throws Exception | loadFont безопасный |
|
||
| A03 | ✅ | Database: race condition на connection | synchronized методы |
|
||
| A13 | ✅ | NekosLifeApi: Thread.sleep в catch | Сон вынесен в retry loop |
|
||
| A14 | ✅ | GenerateRatingsCommand: блокировка event-потока | `complete()` вместо `queue()` |
|
||
| A35 | ✅ | GenerateRatingsCommand: опечатка | `Пропускаемботов` → `Пропускаем ботов` |
|
||
| A04 | ✅ | PrefixCommandRegistrar: God Constructor (22 param) | Разбит на 4 per-feature CommandRegistrar, Multibinder, 1 param |
|
||
| A21 | ✅ | BotModule: путь к БД хардкодом | Вынесен в .env (`DB_PATH`) + BotConfig |
|
||
| A34 | ✅ | CommandCategories: мёртвый код | Уже удалён (старый пакет) |
|
||
| A37 | ✅ | ProfileCommand: регистрация в PrefixCommandRegistrar | LevelCommandRegistrar |
|
||
| A41 | ✅ | TikTok: команда `!tiktok <url>` для скачивания видео | TikTokApi + TikTokCommand + TikTokModule |
|
||
| A42 | ✅ | S1-S5: SOLID рефакторинг | Lifecycle, JdaFactory, PlayLoadHandler, feature modules |
|
||
| A43 | ✅ | D4-D6: DRY рефакторинг | VoiceHelper.connectForPlayback, RatingConfig data-driven, SocialCommand.Flavor |
|
||
| A44 | ✅ | PlayCommand: 207→148 строк | PlayLoadHandler + VoiceHelper.connectForPlayback |
|
||
| A45 | ✅ | TrackScheduler: 146→113 строк | TrackStarter (retry логика) |
|
||
| A46 | ✅ | MuteCommand: 199→173 строки | execute() разбит на 6 методов |
|
||
| A47 | ✅ | BotInitializer: 162→135 строк | JdaFactory + LavalinkManagerFactory |
|
||
| A48 | ✅ | ShutdownManager: 9→3 зависимости | Lifecycle interface + Multibinder |
|
||
| A49 | ✅ | BotModule: 98→84 строки | CommandRegistrar bindings в feature модулях |
|
||
| SP1 | ✅ | Spotify не работал: lavasrc не мог получить токен (spotify-tokener упал с `-32000`, NPE 500 на loadtracks) | Удалён spotify-tokener, lavasrc → client credentials; плейбек Spotify → scsearch-мирроринг (SoundCloud) |
|
||
| PL1 | ✅ | Плейлист SoundCloud: игрался только 1 трек (REPLACED-гонка в TrackScheduler) | Локальный current-трекинг + атомарный skip + сохранение volume (`5e83356`) |
|
||
|
||
---
|
||
|
||
## ♻️ Правила разработки
|
||
|
||
- KISS, DRY, SOLID
|
||
- Файлы ≤150–200 строк (в идеале ≤100)
|
||
- В папке ≤6 файлов
|
||
- Пакет = домен/фича, а не тип файла (`feature/level/`, а не `commands/listeners/`)
|
||
- Каждая фича содержит свои команды, сервисы, репозитории
|
||
- Единый слой персистентности через `repository/`
|
||
- Интеграции в `integration/`
|
||
|
||
---
|
||
|
||
## 🔴 Проблемы (результаты аудита, которые ещё не исправлены)
|
||
|
||
### HIGH
|
||
|
||
| # | Приоритет | Проблема | Статус |
|
||
|---|-----------|----------|--------|
|
||
| A07 | 🟠 **HIGH** | `HealthHttpServer` — single thread executor | ✅ Virtual threads |
|
||
| A08 | 🟠 **HIGH** | Тестовое покрытие ~5% (~34 теста на 149 продакшен-файлов). Нужно 20-30% | ⏳ 268 тестов в 22 файлах |
|
||
| A09 | 🟠 **HIGH** | Нет Docker / docker-compose | ✅ Dockerfile + docker-compose + .env.example |
|
||
| A01 | ⚪ **INFO** | `.env` с токеном в git-истории (локальный репозиторий) | ➖ |
|
||
|
||
### MEDIUM
|
||
|
||
| # | Приоритет | Проблема | Статус |
|
||
|---|-----------|----------|--------|
|
||
| A15 | 🟡 **MEDIUM** | ~33 файла команд в `command/impl/` — не перенесены в `feature/*/commands/` | ✅ 33 файла перенесены |
|
||
| A16 | 🟡 **MEDIUM** | ~16 репозиториев в `database/` — не разнесены в `feature/*/repository/` | ✅ 13+3 репозитория перенесены |
|
||
| A18 | 🟡 **MEDIUM** | `DatabaseMigration` — устаревший код для JSON→SQLite. Нужно оценить | ✅ Harmless, idempotent — keep as-is |
|
||
| A19 | 🟡 **MEDIUM** | Нет статического анализа (checkstyle / spotbugs) | ✅ checkstyle.xml + spotbugs + 169 known violations |
|
||
| A20 | 🟡 **MEDIUM** | Нет версионного каталога (`libs.versions.toml`) | ✅ gradle/libs.versions.toml, build.gradle переписан |
|
||
| A22 | 🟡 **MEDIUM** | Нет пула соединений (HikariCP). Голый JDBC | ✅ HikariCP 5.1.0 + proxy-based prepare() |
|
||
| A24 | 🟡 **MEDIUM** | VoiceTracker, RateLimiter, CooldownManager, MuteScheduler — ScheduledExecutorService. virtual threads? | ✅ Virtual threads |
|
||
| A40 | 🟡 **MEDIUM** | `HelpMenuHandler` в `event/interaction/` — должен быть в `feature/help/` | ✅ Перенесён |
|
||
|
||
### LOW
|
||
|
||
| # | Приоритет | Проблема | Статус |
|
||
|---|-----------|----------|--------|
|
||
| A30 | 🟢 **LOW** | SlowmodeCommand: ручная проверка границ → Math.clamp (другая семантика) | ➖ |
|
||
| A31 | 🟢 **LOW** | ClearCommand: ручная проверка границ → Math.clamp (другая семантика) | ➖ |
|
||
| A39 | 🟢 **LOW** | EmbedFactory: `set*` вместо `with*` для builder | ✅ Renamed in 43 files |
|
||
| A36 | 🟢 **LOW** | RankCardGenerator: URI → URL, deprecated API | ✅ `URI.toURL()` → `new URL()` |
|
||
|
||
### Аудит 30.08.2026 — баг-хант (3 суб-агента, не исправлено)
|
||
|
||
#### Экономика / уровень / модериование / config / voice / counter
|
||
|
||
| # | Приоритет | Проблема | Файл |
|
||
|---|-----------|----------|------|
|
||
| F1 | 🔴 **CRITICAL** | `!slot`: `cashAfter` пишется из устаревшего снимка аккаунта — параллельные спины = бесплатные спины / двойные списания. Нужен атомарный `UPDATE ... SET cash = cash - ? WHERE cash >= ?` | `SlotService.java:45-46` |
|
||
| F2 | 🔴 **CRITICAL** | `transfer()` неатомарный read-check-write без транзакции — двойная трата / потеря денег при параллельных переводах | `EconomyService.java:29-41` |
|
||
| F3 | 🟠 **HIGH** | `/setup` целиком без проверок прав — любой участник меняет счётчик, логи, voice-триггер, роли за уровни | `SetupSlashHandler.java:50-105`, `SetupInteractions.java:19-71` |
|
||
| F4 | 🟠 **HIGH** | `claimDaily()` check-then-act по `last_daily` — двойной дейли при параллельных запросах | `EconomyService.java:88-93` |
|
||
| F5 | 🟠 **HIGH** | `WorkCommand` передаёт `level=0` — работы с `requiredLevel > 0` (COOK/PROGRAMMER/ASTRONAUT) недоступны никогда | `WorkCommand.java:51` |
|
||
| F6 | 🟠 **HIGH** | `LevelResetService`: фильтр `guildId + ":"` не совпадает с ключами `user_stats` — сброс уровней никого не трогает, а одноразовый флаг сгорает | `LevelResetService.java:34-43` |
|
||
| F7 | 🟠 **HIGH** | Blackjack: `hit/stand` NPE при гонке (state==null после удаления), общий несинхронизированный ArrayList карт | `BlackjackCommand.java:125`, `BlackjackService.java:52-95` |
|
||
| F8 | 🟠 **HIGH** | Блокирующие JDBC / `.complete()` на gateway-потоке JDA: 4 SQL на каждое сообщение, SQL на join/leave, REST на создание канала | `MessageListener.java:42-48`, `MemberCounterListener.java:59-74`, `FishMessageListener.java:39`, `VoiceChannelListener.java:41`, `VoiceTracker.java:78-79` |
|
||
| F9 | 🟡 **MEDIUM** | Blackjack push (равные очки) — игрок теряет ставку, ничья не обработана | `BlackjackService.java:89` |
|
||
| F10 | 🟡 **MEDIUM** | Blackjack TOCTOU `containsKey` — двойное списание ставки при параллельных `!bj`; ставка теряется при рестарте (игры только в памяти) | `BlackjackService.java:22,48` |
|
||
| F11 | 🟡 **MEDIUM** | Рыбалка: кулдаун пишется только при верном ответе — можно бесконечно перегенерировать поле без штрафа | `FishingService.java:24-30,87` |
|
||
| F12 | 🟡 **MEDIUM** | Счётчик участников: `getMemberCount()` (с ботами) vs `countMembers()` (без) — число на канале прыгает | `CounterChannelUpdater.java:22`, `CounterManager.java:81` |
|
||
| F13 | 🟡 **MEDIUM** | XP/время в войсе: read-modify-write без синхронизации — терянные апдейты при параллельных начислениях | `LevelService.java:41-49` |
|
||
| F14 | 🟡 **MEDIUM** | `getSystemChannel()` @Nullable → NPE при level-up в войсе | `VoiceTracker.java:85` |
|
||
|
||
#### Music / radio
|
||
|
||
| # | Приоритет | Проблема | Файл |
|
||
|---|-----------|----------|------|
|
||
| M1 | 🟠 **HIGH** | Утечка потоков: `removeMusicManager()` не вызывает `destroy()` — каждый автолив оставляет навсегда живой `ScheduledExecutorService` | `LavalinkManager.java:96-98`, `AutoLeaveController.java:47` |
|
||
| M2 | 🟠 **HIGH** | `m!play`: блокирующий `connectForPlayback` (sleep до ~4с) прямо на gateway-потоке music-бота | `MusicBotPlayHandler.java:36` |
|
||
| M3 | 🟠 **HIGH** | Селект-меню поиска: то же — блокирующий `connectForPlayback` в `onStringSelectInteraction` | `PlaySelectHandler.java:55` |
|
||
| M4 | 🟡 **MEDIUM** | `asVoiceChannel()` кидает `IllegalStateException` в stage-каналах — команды падают у пользователей | `VoiceHelper.java:59`, `PlaySlashHandler.java:31`, `RadioBotCommands.java:40` |
|
||
| M5 | 🟡 **MEDIUM** | Гонка `RadioManager.startRadio` — двойной `!r!play` теряет GuildMusicManager (утечка), `connect()==null` оставляет «зомби-сессию» | `RadioManager.java:64-83` |
|
||
| M6 | 🟡 **MEDIUM** | Отложенный `loadItem` оживляет уничтоженное радио (enqueue на выключенном executor → `RejectedExecutionException` глотается) | `RadioStreamLoader.java:36` |
|
||
| M7 | 🟡 **MEDIUM** | `!queue`: embed-поле >1024 символов при длинных названиях → `IllegalArgumentException`, команда падает | `QueueView.java:29,51-64` |
|
||
| M8 | 🟢 **LOW** | `Integer.parseInt` переполняется на большом «номере трека» из поиска | `PlayCommand.java:83` |
|
||
| M9 | 🟢 **LOW** | Заголовок embed поиска не обрезан до 256 символов | `SearchHandler.java:50` |
|
||
| M10 | 🟢 **LOW** | TOCTOU `link.getCachedPlayer() != null ? ...getX()` — NPE между проверкой и вызовом | `QueueView.java:40`, `VolumeControl.java:11-22` |
|
||
| M11 | 🟢 **LOW** | `FilterState.customBands` — несинхронизированный ArrayList под конкурентным доступом | `FilterState.java:19,89` |
|
||
| M12 | 🟢 **LOW** | `m!play` игнорирует неудачу подключения — «Добавлено в очередь», а звука нет | `MusicBotPlayHandler.java:36` |
|
||
| M13 | 🟢 **LOW** | `destroy()` глотает `InterruptedException` без восстановления флага | `GuildMusicManager.java:64-66` |
|
||
| M14 | 🟢 **LOW** | `r!volume` рапортует успех при отсутствии радио-сессии | `RadioSubcommands.java:108-113` |
|
||
|
||
#### Core / database
|
||
|
||
| # | Приоритет | Проблема | Файл |
|
||
|---|-----------|----------|------|
|
||
| C1 | 🟠 **HIGH** | Прокси `PreparedStatement` не разворачивает `InvocationTargetException` → SQL-ошибки вылетают как `UndeclaredThrowableException`, все `catch (SQLException)` их не ловят | `Database.java:76-89` |
|
||
| C2 | 🟡 **MEDIUM** | Переполнение long в `MuteDurationParser` (`!mute 106751991167301d` → отрицательные секунды) | `MuteDurationParser.java:55-57` |
|
||
| C3 | 🟡 **MEDIUM** | `event.getGuild()` в ЛС кидает `IllegalStateException` (JDA 6.5.0) — префикс-команды в ЛС падают | `EventDispatcher.java:81,107` |
|
||
| C4 | 🟡 **MEDIUM** | `CooldownManager` check-then-act — два параллельных `!rep`/`!rob` обходят кулдаун | `CooldownManager.java:23-57` |
|
||
| C5 | 🟡 **MEDIUM** | Миграция продолжается после частичного провала, `migrated=true` ставится всегда — тихая потеря данных | `DatabaseMigration.java:40-44`, `JsonMigration.java:43,63,88,117` |
|
||
| C6 | 🟢 **LOW** | `PRAGMA foreign_keys = ON` выполняется на одном соединении, не на всех в пуле | `Database.java:43-48` |
|
||
| C7 | 🟢 **LOW** | `JsonMigration`: NPE на битых записях легаси-JSON → бот не стартует | `JsonMigration.java:33-39` |
|
||
| C8 | 🟢 **LOW** | Lavalink-клиенты не регистрируются в ShutdownManager — websocket не закрывается при graceful shutdown | `BotInitializer.java:73-83` |
|
||
| C9 | 🟢 **LOW** | Ответ «команда не найдена» до rate-limiter — флуд несуществующими командами не ограничен | `EventDispatcher.java:63-77` |
|
||
| C10 | 🟢 **LOW** | `ApiRequestQueue`: `future.get()` без таймаута; при прерывании возвращает `null` | `ApiRequestQueue.java:38` |
|
||
|
||
---
|
||
|
||
## 📋 План рефакторинга
|
||
|
||
### Этап 1: Java 21 ✅ (14 items)
|
||
- [x] EventDispatcher: VirtualThreadPool
|
||
- [x] instanceof pattern matching (3 файла)
|
||
- [x] arrow-switch (InfoCommand)
|
||
- [x] String.repeat() (RateCommand)
|
||
- [x] .toList() (3 файла)
|
||
- [x] Map.ofEntries() (HelpCategories)
|
||
|
||
### Этап 2: Рефакторинг PrefixCommandRegistrar ✅
|
||
- [x] Создан интерфейс `CommandRegistrar`
|
||
- [x] 4 per-feature регистратора
|
||
- [x] Guice Multibinder для сбора регистраторов
|
||
- [x] 22 параметра → 1 (`Set<CommandRegistrar>`)
|
||
|
||
### Этап 3: Critical-баги ✅
|
||
- [x] RankCardGenerator: async + try-with-resources + cache + SVG
|
||
- [x] Database: synchronized connection
|
||
- [x] NekosLifeApi: чистый retry loop
|
||
- [x] GenerateRatingsCommand: async через complete()
|
||
- [x] DB path: вынесен в .env
|
||
|
||
### Этап 4: Перемещение файлов в feature-пакеты ✅
|
||
- [x] `command/impl/utility/` → `feature/*/commands/`
|
||
- [x] `command/impl/entertainment/` → `feature/*/commands/`
|
||
- [x] `command/entertainment/social/` → `feature/social/commands/`
|
||
- [x] `database/*/` → `feature/*/repository/`
|
||
- [x] `event/interaction/HelpMenuHandler` → `feature/help/`
|
||
|
||
### Этап 5: Тесты (цель: 20-30% покрытие)
|
||
- [x] Тесты на репозитории (in-memory SQLite) — Config, ShipPair, UserStats, Ratings, Warnings, ActiveMutes
|
||
- [x] Unit-тесты сервисов — MessageCooldownService, LevelService (был), XpFormula, ChoiceOptionParser
|
||
- [x] Unit-тесты value objects — UserStats (был), LevelProgress, LevelUpResult, BotConfig
|
||
- [x] Тесты embed — EmbedFactory (builder chaining), EmbedTemplates (factory methods)
|
||
- [x] Тесты форматирования — LeaderboardFormatter
|
||
- [ ] Unit-тесты на команды (с моками JDA)
|
||
- [ ] Smoke-тест: все команды регистрируются без коллизий
|
||
- [ ] Тесты на RankCardGenerator (изолированные, без сети)
|
||
- [ ] Тесты на EventDispatcher (маршрутизация, rate limiting)
|
||
|
||
### Этап 6: Инфраструктура ✅
|
||
- [x] Dockerfile (multi-stage)
|
||
- [x] docker-compose.yml
|
||
- [x] Статический анализ: checkstyle / spotbugs
|
||
- [x] Версионный каталог: `gradle/libs.versions.toml`
|
||
- [x] HikariCP вместо голого JDBC
|
||
|
||
### Этап 7: Кодстайл
|
||
- [x] EmbedFactory: `set*` → `with*` для builder
|
||
- [ ] DatabaseMigration: оценить нужность
|
||
- [x] HelpMenuHandler: переместить в feature/help/
|
||
|
||
---
|
||
|
||
## 🚀 Новые фичи (Roadmap)
|
||
|
||
## 🚀 Экономика — ✅ реализовано
|
||
|
||
### Модель данных
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS economy (
|
||
user_id TEXT PRIMARY KEY,
|
||
cash INTEGER NOT NULL DEFAULT 0, -- на руках
|
||
bank INTEGER NOT NULL DEFAULT 0, -- в банке
|
||
total_earned INTEGER NOT NULL DEFAULT 0,
|
||
total_spent INTEGER NOT NULL DEFAULT 0,
|
||
last_daily TEXT, -- ISO date
|
||
daily_streak INTEGER DEFAULT 0,
|
||
fishing_luck INTEGER DEFAULT 0, -- stat lvl 0-5
|
||
efficiency INTEGER DEFAULT 0, -- stat lvl 0-5
|
||
last_fish TEXT, -- ISO datetime
|
||
job TEXT, -- название работы
|
||
last_work TEXT -- ISO datetime
|
||
);
|
||
```
|
||
|
||
### Заработок
|
||
|
||
| Механика | Доход | Кулдаун |
|
||
|---|---|---|
|
||
| Сообщение | 1–3 LC | 60с |
|
||
| Голос | 1 LC / 2 мин | 2 мин |
|
||
| Ежедневно | 50 LC + streak ×1.5 на 7й день | 24ч |
|
||
| +rep (полученная) | 10 LC | — |
|
||
| Рыбалка | base × speed_mul × (1 + fishing_luck×0.01) | 2ч |
|
||
| Работа | random(range) × (1 + efficiency×0.01) | 6ч |
|
||
| Блэкджек | ×1.5 при победе | — |
|
||
| Слоты | ×0–×50 | — |
|
||
|
||
### Миниигры
|
||
|
||
**Рыбалка** (`!fish easy/normal/hard`)
|
||
- Сетка N×N (emoji + 🌊), бесплатно, неправильный ответ = 0
|
||
- Множитель скорости: <50% времени = ×1.5, <30% = ×2.0
|
||
- Стат `fishing_luck` (макс 5, стоимость 800×lvl)
|
||
|
||
**Блэкджек** (`!bj <bet>`)
|
||
- Дилер стоит на 16, ничья = проигрыш, BJ = ×1.5
|
||
- Кнопки Hit/Stand
|
||
|
||
**Слоты** (`!slot <bet>`)
|
||
- 🍒🍒🍒 ×3 (10%), 🔔🔔🔔 ×5 (5%), 💎💎💎 ×10 (2%), 👑👑👑 ×15 (1%), ⚡⚡⚡ ×50 (0.1%)
|
||
|
||
**Работы** (`!job list`, `!job join`, `!work`)
|
||
| Работа | Ур. | Доход |
|
||
|---|---|---|
|
||
| 🪓 Лесоруб | — | 15–30 |
|
||
| ⛏️ Шахтёр | — | 20–40 |
|
||
| 🎣 Рыбак | — | 25–50 |
|
||
| 🍳 Повар | 15 | 30–60 |
|
||
| 💻 Программист | 20 | 40–80 |
|
||
| 🧑🚀 Космонавт | 35 | 50–100 |
|
||
|
||
Стат `efficiency` (макс 5, стоимость 800×lvl)
|
||
|
||
**Rob** (`!rob @user`)
|
||
- 24ч кулдаун, 40% успех, крадёт до 15% cash (макс 10к)
|
||
- Провал: штраф 10% cash (макс 5к) → жертве
|
||
- Банк не трогается
|
||
|
||
**Банк** (`!bank deposit/withdraw/balance`)
|
||
- Без процентов, защита от rob
|
||
|
||
### Структура пакета
|
||
|
||
```
|
||
feature/economy/
|
||
EconomyModule.java
|
||
EconomyCommandRegistrar.java
|
||
domain/
|
||
EconomyAccount.java
|
||
EconomyTransaction.java
|
||
Job.java
|
||
SlotCombination.java
|
||
repository/
|
||
EconomyRepository.java
|
||
SqlEconomyRepository.java
|
||
service/
|
||
EconomyService.java
|
||
FishingService.java
|
||
BlackjackService.java
|
||
SlotService.java
|
||
JobService.java
|
||
command/
|
||
core/ (Balance, Baltop, Pay, Daily)
|
||
minigame/ (Fish, Blackjack, Slot, Job, Work)
|
||
social/ (Rob)
|
||
bank/ (Bank)
|
||
```
|
||
|
||
---
|
||
|
||
### Приоритет 1
|
||
- [x] Система экономики (Economy) — **✅**
|
||
|
||
### Приоритет 2
|
||
- [x] Авто-модерация (3 варна = авто-мут на 1ч)
|
||
- [x] Музыка (lavaplayer: play, skip, stop, queue, np, volume, leave)
|
||
- [x] ~~Музыка — поиск с выбором (yt/sc/list + выбор номера)~~
|
||
- [x] Server Stats (участники, каналы, роли, бусты)
|
||
- [x] Level Roles (роли за уровни)
|
||
- [x] Красивые level-up embed с прогресс-баром
|
||
|
||
### Приоритет 3
|
||
- [x] Статистика сервера — графики, CSV export — ✅ `!serverstats [csv|chart]`
|
||
|
||
---
|
||
|
||
### 🐾 Животные — зоо-викторины и картинки (public-apis) — 🟢 в работе
|
||
|
||
- [x] `feature/animals/AnimalApi` — клиент к 8 API (thecatapi, dog.ceo, randomfox, shibe.online, some-random-api, nekos.life, waifu.pics, catboys.com) с fallback-цепочкой
|
||
- [x] `!catperson` / `!dogperson` / `!foxperson` — «Какой ты кот/пёс/лис»: % + роль + картинка (детерминировано ID)
|
||
- [x] `!shibe` / `!birb` — картинки шиба-ину и птичек
|
||
- [x] `!neko` / `!waifu` / `!catboy` — аниме-картинки (неко, вайфу, кошко-мальчик)
|
||
- [x] Факты: `!catfact` / `!dogfact` — факт + картинка (catfact.ninja, dog facts API)
|
||
- [x] `!joke` — анекдоты (jokeapi.dev, категории Any/Programming/Misc)
|
||
- [x] `!chuck` — цитаты Чака Норриса (api.chucknorris.io)
|
||
- [x] `!inspire` — мотивационные цитаты (zenquotes.io)
|
||
- [x] `!apod` — астрономическая картинка дня NASA (api.nasa.gov, DEMO_KEY)
|
||
- [x] `!iss` — где сейчас МКС (api.wheretheiss.at)
|
||
- [x] `!space` — кто сейчас в космосе (open-notify.org)
|
||
- [x] `!xkcd` — свежий стрип (xkcd.com/api)
|
||
- [x] `!meme <subreddit>` — расширить мем по сабреддиту
|
||
|
||
---
|
||
|
||
## ⚡ Стабильность и скорость (2026-08-19, аудит сервера 185.176.94.38)
|
||
|
||
### Найдено в логах
|
||
| # | Проблема | Статус |
|
||
|---|----------|--------|
|
||
| S01 | `LogEmbeds.ban`/`kick` — NPE `Value may not be null`, если бан/кик без причины (падал onSuccess-колбэк, терялся лог) | ✅ `reason != null ? reason : "Не указана"` |
|
||
| S02 | `RatingService.getDeterministicValue` — гонка на общем `Random.setSeed()`: 2 пользователя одновременно → результаты «подменялись» | ✅ локальный `new Random(seed)` (thread-safe) |
|
||
| S03 | Пул репортов на внешние API отсутствовал | ✅ `util/ApiRequestQueue` (пул 4, изоляция ответов) |
|
||
| S04 | `Missed 2 heartbeats` — 1 раз, транзиентный (сеть), не повторяется | ➖ наблюдать |
|
||
|
||
### Что сделано
|
||
- `util/ApiRequestQueue` — очередь исходящих запросов: каждый запрос выполняется изолированно, ответ не может подмешаться к соседнему, пул 4 не перегружает публичные API. Подключён в `AnimalApi` и `NekosLifeApi`, зарегистрирован в LifecycleModule (graceful shutdown)
|
||
- `RatingService` — детерминированное значение теперь без общего `Random` (root cause «подмены ответов»)
|
||
- `LogEmbeds.ban/kick` — дефолт причины «Не указана»
|
||
|
||
### Производительность (наблюдения)
|
||
- Команды выполняются за 0–100 мс (virtual threads), CPU ~0.2%, RAM 186 МБ — узких мест нет
|
||
- Основная задержка — внешние HTTP API (таймауты 10 с + retry с бэк-оффом); очередь ограничивает параллелизм до 4
|
||
|
||
### Оптимизации (2026-08-19)
|
||
- [x] `AnimalApi` — Caffeine-кэш успешных URL (TTL 30 с): повторные `!neko`/`!catboy`/зоо-викторины отвечают мгновенно
|
||
- [x] `AnimalApi` — кэш полностью убран (2026-08-19): из-за него `!neko` 30с отдавал одну и ту же фотку
|
||
- [x] `MemeCommand` — HttpURLConnection (без таймаутов, мог висеть) → OkHttp 6с/8с
|
||
- [ ] `Database` — включить `PRAGMA journal_mode = WAL` (сейчас нет — блокировки при конкурентной записи)
|
||
- [ ] NekosLifeApi — таймауты/ретраи вынести в конфиг (10с → 8с, retry 3 → 2)
|
||
- [ ] Негативный кэш неудачных запросов в AnimalApi (не долбить упавший API 30с)
|
||
- [ ] Dockerfile — JVM флаги (`-Xmx512m -XX:+UseG1GC -XX:+UseStringDeduplication`)
|
||
- [ ] Разобрать остальные `HttpURLConnection` на общий OkHttp-клиент
|
||
|
||
---
|
||
|
||
## 🚧 v2.5 — Java 25, рейтинги, память (2026-08-19)
|
||
|
||
### 1. Java 26 + Gradle 9.7.0
|
||
- [x] `build.gradle`: `toolchain { languageVersion = 26 }` — Gradle сам находит JDK 26 (`/usr/lib/jvm/java-26-openjdk`), `export JAVA_HOME` больше не нужен
|
||
- [x] `.java-version` (файл `26`) — для IDE/jenv/asdf
|
||
- [x] `gradle/wrapper/gradle-wrapper.properties`: 9.3.1 → 9.7.0 (системный gradle 9.7.0 в `/usr/bin/gradle`)
|
||
- [x] `Dockerfile`: `eclipse-temurin:21-jdk-alpine` → `26-jdk-alpine`, `21-jre-alpine` → `26-jre-alpine`
|
||
- [x] Mockito 5.15.2 тянет ByteBuddy 1.14.x (не знает класс-файлы major 70) → добавить `byte-buddy:1.18.8` (уже в кэше, поддержка Java 26)
|
||
- [x] CI workflows (ci/docker-publish/release): `java-version: "21"` → `"26"`
|
||
- [x] Пересобрать локально + `spotlessApply` (Spotless 6.25→8.10.0, SpotBugs 4.9.3→4.10.3 для major 70; 458 тестов ✅)
|
||
|
||
### 1.1 Обновление зависимостей (2026-08-19, все 20)
|
||
- [x] jda 6.4.2→6.5.0, lavaplayer 2.2.6→2.2.7, logback 1.5.18→1.6.3, caffeine 3.2.0→3.2.4
|
||
- [x] sqlite 3.49.1.0→3.53.2.1, **hikaricp 6.3.0→7.1.0** (мажор, API совместим, тесты ✅), okhttp 5.4.0→5.5.0
|
||
- [x] junit 5.12.1→5.13.4, junit-platform-launcher 1.12.1→1.13.4, mockito 5.15.2→5.23.0, bytebuddy 1.18.8→1.18.12
|
||
- [x] **slf4j 2.1.0-alpha1 → 2.0.18** (стабильный вместо alpha)
|
||
- [x] checkstyle 10.23.0→13.10.0 (toolVersion, требует Java 21+ ✓), spotbugsPlugin 6.1.7→6.4.8, jsoup 1.19.1→1.22.2
|
||
- [x] gson 2.14.0, guice 7.0.0, lavalinkClient 3.4.0, dotenv 3.2.0, spotbugs 4.10.3 — актуальные, без изменений
|
||
|
||
### 2. `!generateratings` генерит только на 2 человек
|
||
- [x] **Причина**: JDA `MemberCachePolicy.DEFAULT` кэширует только голосовых/owner/self + `ChunkingFilter` по умолчанию чанкует только гильдии ≥250 участников → `guild.getMembers()` = 2
|
||
- [x] `core/JdaFactory.java`: `.setMemberCachePolicy(MemberCachePolicy.ALL)` + `.setMemberChunkingFilter(ChunkingFilter.ALL)`
|
||
- [x] `GenerateRatingsCommand.java`: `guild.getMembers()` → `guild.loadMembers().get()` (подстраховка)
|
||
|
||
### 3. Команда топа/низа рейтингов
|
||
- [x] `RatingsRepository`: `getTopRatings(guildId, type, limit)` + `getBottomRatings(...)` (ORDER BY value DESC/ASC LIMIT)
|
||
- [x] Новая команда `feature/rating/commands/board/RatingBoardCommand` — флейворы `ratingstop`/`ratingbottom` (высшие/низшие), рендер `<@id> — value` без резолва участников, медали топ-3
|
||
- [x] Зарегистрировать в `feature/utility/UtilityCommandRegistrar` (рядом с GenerateRatingsCommand)
|
||
|
||
### 4. Лимит памяти 1 ГБ
|
||
- [x] `docker-compose.yml`: сервис `bot` → `mem_limit: 1g`
|
||
- [x] `Dockerfile` ENTRYPOINT: JVM-флаги `-Xmx512m -XX:+UseG1GC -XX:+UseStringDeduplication`
|
||
|
||
### 5. Дальнейшие оптимизации (поиск)
|
||
- [x] Изучено: Gradle 9.4+ нужен для Java 26 (9.7.0 ✓); Gradle 8.14.3 падает на major 69/70; JDA — `loadMembers()` требует GUILD_MEMBERS intent (у бота включён)
|
||
- [ ] `Database`: `PRAGMA journal_mode = WAL`
|
||
- [ ] NekosLifeApi: таймауты/ретраи в конфиг
|
||
- [ ] Негативный кэш в AnimalApi
|
||
- [ ] Свести все HttpURLConnection на OkHttp
|
||
|
||
---
|
||
|
||
## 🔴 Результаты аудита KISS/DRY/SOLID (2026-07-04)
|
||
|
||
### DRY — дублирование кода (критично)
|
||
|
||
| # | Что дублируется | Где | Сколько раз | Статус |
|
||
|---|----------------|-----|-------------|--------|
|
||
| D1 | `getDeterministicValue()` — целиком | GayCommand, FatCommand, IqCommand, PpCommand, BoykisserCommand, MustacheCommand, RateCommand | 7 | ✅ RatingService |
|
||
| D2 | Cooldown check boilerplate (5 строк) | Все entertainment команды | 10+ | ✅ RatingService |
|
||
| D3 | `trunc()` / `err()` helpers | MuteCommand, WarnCommand, SwapAndCaseCommand, UnmuteCommand, PunishCommand | 5 | ✅ ModerationHelper |
|
||
| D4 | Voice connect retry loop (цикл 10×200ms) | PlayCommand, RadioManager | 2 | ✅ VoiceHelper.connectForPlayback() |
|
||
| D5 | RatingConfig — 6 методов с идентичной структурой | RatingConfig.java | 6 | ✅ Data-driven getLevels(name) + findLevel(value, name) |
|
||
| D6 | Social команды (hug/kiss/slap/poke/tickle/cuddle/feed/pat) — шаблон | 10 файлов | 10 | ✅ SocialCommand.Flavor (7 команд), pat/compliment/roast — отдельные |
|
||
|
||
### SOLID
|
||
|
||
| # | Нарушение | Файл | Описание |
|
||
|---|-----------|------|----------|
|
||
| S1 | SRP | BotModule.java | Биндит всё приложение | ✅ Перенесён в feature модули + LifecycleModule |
|
||
| S2 | SRP | SwapAndCaseCommand.java | Две разные команды в одном файле | ✅ Уже разделены: SwapCommand + CaseLookupCommand |
|
||
| S3 | SRP | BotInitializer.java | Инициализирует 3 JDA, Lavalink, mutes, counters, health | ✅ JdaFactory + LavalinkManagerFactory |
|
||
| S4 | SRP | ShutdownManager.java | 9 зависимостей в конструкторе | ✅ Lifecycle interface, 3 зависимости |
|
||
| S5 | SRP | PlayCommand.java | execute() на 145 строк | ✅ PlayLoadHandler + VoiceHelper.connectForPlayback() |
|
||
| S6 | OCP | Meter-команды (gay/fat/iq/pp) | Добавить новую = копировать 90 строк | ✅ RatingService |
|
||
| S7 | DIP | MuteCommand, WarnCommand | >5 конкретных репозиториев вместо интерфейсов | ✅ Репозитории уже интерфейсы (ModerationCaseRepository, ActiveMuteRepository, WarningRepository) |
|
||
|
||
### Файлы >150 строк
|
||
|
||
| # | Файл | Строк |
|
||
|---|------|-------|
|
||
| F1 | PlayCommand.java | 148 ✅ |
|
||
| F2 | QueueSlashHandler.java | 207 |
|
||
| F3 | MusicBotControlHandler.java | 207 |
|
||
| F4 | QueueCommands.java | 207 |
|
||
| F5 | TrackScheduler.java | 113 ✅ |
|
||
| F6 | MuteCommand.java | 173 ✅ |
|
||
| F7 | ModerationLogger.java | 199 |
|
||
| F8 | FeatureTables.java | 193 |
|
||
| F9 | DatabaseMigration.java | 193 |
|
||
| F10 | VoiceChannelManager.java | 187 |
|
||
| F11 | ServerStatsService.java | 187 |
|
||
| F12 | VoiceSetupPrefixCommand.java | 183 |
|
||
| F13 | RadioManager.java | 182 |
|
||
| F14 | MetricsCollector.java | 182 |
|
||
| F15 | WarnCommand.java | 184 |
|
||
| F16 | BotInitializer.java | 135 ✅ |
|
||
| F17 | SlashHandler's (Queue/Filter) | >100 |
|
||
| F18 | GenerateRatingsCommand.java | 172 |
|
||
| F19 | BlackjackService.java | 172 |
|
||
| F20 | SqlEconomyRepository.java | 169 |
|
||
| F21 | GuildMusicManager.java | 167 |
|
||
| F22 | ProfileCommand/RankCardGen | >150 |
|
||
|
||
### >3 файла в папке
|
||
|
||
| # | Папка | Файлов |
|
||
|---|-------|--------|
|
||
| P1 | feature/entertainment/commands/ | 15 |
|
||
| P2 | feature/social/commands/ | 10 |
|
||
| P3 | feature/economy/command/minigame/ | 7 |
|
||
| P4 | feature/level/ (все субпапки) | 21 |
|
||
| P5 | feature/moderation/ (все субпакеты) | 18 |
|
||
| P6 | feature/music/ (все субпакеты) | 22 |
|
||
| P7 | feature/voice/ (все субпакеты) | 12 |
|
||
|
||
### Нет тестов
|
||
|
||
| # | Фича | Файлов |
|
||
|---|------|--------|
|
||
| T1 | music | 20+ |
|
||
| T2 | voice | 12 |
|
||
| T3 | social | 10 |
|
||
| T4 | role | 9 |
|
||
| T5 | radio | 5 |
|
||
| T6 | reputation | 4 |
|
||
| T7 | counter | 7 |
|
||
| T8 | admin | 1 |
|
||
| T9 | help | 2 |
|
||
| T10 | utility | 2 |
|
||
| T11 | welcome | 2 |
|
||
| T12 | profile | 5 |
|
||
| T13 | serverstats | 1 |
|
||
|
||
## 📋 План рефакторинга (KISS/DRY/SOLID)
|
||
|
||
### Этап 8: Убить дублирование (DRY) — ✅ завершён
|
||
|
||
- [x] D1/D2: `RatingService` с `getDeterministicValue()`, cooldown, member parsing — 7 команд переписаны
|
||
- [x] D3: `ModerationHelper` с `trunc()`/`err()` — MuteCommand, UnmuteCommand, WarnCommand, SwapAndCaseCommand, PunishCommand
|
||
- [x] D4: Вынести voice connect retry в утилиту — ✅ VoiceHelper.connectForPlayback()
|
||
- [x] D5: RatingConfig — заменить 6 методов на data-driven (Map или config) — ✅ getLevels(name) + findLevel(value, name)
|
||
- [x] D6: Social команды — единый `SocialCommand` с параметром action — ✅ SocialCommand.Flavor (7 команд)
|
||
|
||
### Этап 9: SOLID — ✅ завершён
|
||
|
||
- [x] S1: Разбить `BotModule` на per-feature модули (FeatureModule → install в BotModule) — ✅ 8 feature модулей + LifecycleModule
|
||
- [x] S2: Разделить `SwapAndCaseCommand` на два отдельных файла — ✅ Already split (SwapCommand + CaseLookupCommand)
|
||
- [x] S3: Выделить `MusicBotFactory`, `RadioBotFactory` из `BotInitializer` — ✅ JdaFactory + LavalinkManagerFactory
|
||
- [x] S4: Уменьшить кол-во зависимостей `ShutdownManager` через события (EventBus) — ✅ Lifecycle interface (9→3 deps)
|
||
- [x] S5: Разбить `PlayCommand.execute()` на методы — ✅ PlayLoadHandler + VoiceHelper.connectForPlayback()
|
||
- [x] S6: Meter-команды сделать data-driven через `RatingService` — ✅ RatingConfig.findLevel(value, name)
|
||
- [x] S7: Выделить интерфейсы для репозиториев/сервисов (DIP) — ✅ Already interfaces
|
||
|
||
### Этап 10: Файлы ≤150 строк — в процессе
|
||
|
||
- [x] F01: PlayCommand.java — разбить на методы/классы — ✅ 207→148 строк
|
||
- [ ] F02-04: QueueSlashHandler, MusicBotControlHandler, QueueCommands — вынести общие части
|
||
- [x] F05: TrackScheduler.java — вывести FilterState в отд. файл или утилиту — ✅ 146→113 строк (TrackStarter)
|
||
- [x] F06: MuteCommand.java — разбить на subcommand handler'ы — ✅ 199→173 строки
|
||
- [ ] F07: ModerationLogger.java — разбить по типам событий
|
||
- [ ] F08-09: FeatureTables, DatabaseMigration — разбить по фичам
|
||
- [ ] F10-12: Voice* — вынести логику в сервисы
|
||
- [ ] F13: RadioManager.java — вынести voice connect в утилиту
|
||
- [ ] F14: MetricsCollector.java — разбить по типам метрик
|
||
- [ ] F15: WarnCommand.java — вынести auto-mute в отдельный сервис
|
||
- [x] F16: BotInitializer.java — вынести фабрики ботов — ✅ 162→135 строк
|
||
- [x] F02-04: QueueSlashHandler, MusicBotControlHandler, QueueCommands — вынести общие части — ✅ вынесены при реорге music
|
||
- [ ] Остальные: разбить по SOLID
|
||
|
||
### Этап 11: ≤6 файлов в папке
|
||
|
||
- [x] P1: Entertainment команды — уже сгруппированы (choice/, random/; rating-команды в feature/rating/)
|
||
- [x] P2: Social команды — уже объединены (SocialCommand + 3 отдельных), 4 файла
|
||
- [x] P4: Level — уже разбит на commands/, service/, repository/, domain/
|
||
- [x] P5: Moderation — уже разбит по поддоменам (punish, utils, warn, caselookup, log, repository)
|
||
- [x] P6: Music — ✅ commands/filter|control|queue, listener/bot|select, manage/player|filter, util/embed|voice|query (все папки ≤6)
|
||
|
||
### Этап 12: Тесты
|
||
|
||
- [ ] T1: Music — написать тесты на LavalinkManager, TrackScheduler, PlayCommand
|
||
- [ ] T2: Voice — VoiceChannelManager, VoiceButtonHandler
|
||
- [ ] T3: Social — интеграционные тесты с NekosLifeApi (мок)
|
||
- [ ] T4: Radio — RadioManager, RadioCommand
|
||
- [ ] T5: Role — CustomRoleManager, RoleCommandValidator
|
||
- [ ] T6-13: Остальные фичи — минимум smoke test
|
||
|
||
### Техдолг (баги)
|
||
|
||
- [ ] B1: EmbedFactoryTest — 2 падающих теста (`withTitle`, `withColor`) — IllegalStateException
|
||
- [ ] B2: SlowmodeCommandTest — починен (было 7 ошибок компиляции из-за JDA 6.4 SelfMember)
|
||
|
||
---
|
||
|
||
**Версия:** 2.4 | **Последнее обновление:** 2026-07-14 | **Java 21 ✅, God Constructor ✅, Critical баги ✅, Economy ✅, Level Roles ✅, Lavalink Music ✅, SpotBugs ✅, Checkstyle ✅, CI/CD ✅, SOLID ✅, TikTok ✅**
|
||
|
||
## ✅ Что добавлено в v2.4
|
||
|
||
### 🎬 TikTok Download Command
|
||
- [x] Команда `!tiktok <url>` — скачивание видео из TikTok без watermark
|
||
- [x] TikTokApi — сервис для работы с tikwm.com API (OkHttp + Gson)
|
||
- [x] Скачивание через virtual threads
|
||
- [x] Embed с информацией (title, author, duration, cover)
|
||
- [x] Проверка размера файла (< 8MB)
|
||
- [x] TikTokModule для DI
|
||
|
||
### 🔧 Рефакторинг SOLID/DRY
|
||
- [x] **S1**: BotModule — CommandRegistrar bindings перенесены в feature модули (8 модулей)
|
||
- [x] **S2**: SwapAndCaseCommand — уже разделены (SwapCommand + CaseLookupCommand)
|
||
- [x] **S3**: BotInitializer — извлечены JdaFactory + LavalinkManagerFactory (162→135 строк)
|
||
- [x] **S4**: ShutdownManager — Lifecycle interface, 9→3 зависимости (118→89 строк)
|
||
- [x] **S5**: PlayCommand — извлечён PlayLoadHandler + VoiceHelper.connectForPlayback() (207→148 строк)
|
||
- [x] **S7**: DIP — репозитории уже интерфейсы (ModerationCaseRepository, ActiveMuteRepository, WarningRepository)
|
||
- [x] **D4**: Voice connect retry — VoiceHelper.connectForPlayback()
|
||
- [x] **D5**: RatingConfig — data-driven getLevels(name) + findLevel(value, name)
|
||
- [x] **D6**: Social команды — SocialCommand.Flavor (7 команд), pat/compliment/roast — отдельные
|
||
- [x] **TrackScheduler** — извлечён TrackStarter (146→113 строк)
|
||
- [x] **MuteCommand** — execute() разбит на методы (199→173 строки, execute() 23 строки)
|
||
|
||
### 📦 Новые файлы
|
||
- `feature/tiktok/TikTokApi.java` — API сервис
|
||
- `feature/tiktok/TikTokCommand.java` — команда
|
||
- `feature/tiktok/TikTokModule.java` — Guice модуль
|
||
- `core/Lifecycle.java` — интерфейс для shutdown
|
||
- `core/JdaFactory.java` — фабрика JDA
|
||
- `core/LavalinkManagerFactory.java` — фабрика LavalinkManager
|
||
- `core/LifecycleModule.java` — Multibinder для Lifecycle
|
||
- `feature/entertainment/EntertainmentModule.java` — CommandRegistrar binding
|
||
- `feature/utility/UtilityModule.java` — CommandRegistrar binding
|
||
- `feature/music/play/PlayLoadHandler.java` — обработчик загрузки треков
|
||
- `feature/music/manage/TrackStarter.java` — retry логика запуска треков
|
||
|
||
---
|
||
|
||
## ✅ Что добавлено в v2.3
|
||
|
||
### 🎵 Lavalink Music Service
|
||
- [x] Node.js сервис с 4 Discord ботами (kazagumo + shoukaku)
|
||
- [x] Поддержка YouTube, SoundCloud, радио (`!radio <url>`)
|
||
- [x] Команды: play, stop, skip, queue, np, volume, pause, resume, shuffle, repeat, clear, move
|
||
- [x] Lavalink контейнер с healthcheck
|
||
- [x] docker-compose с интеграцией в основной проект
|
||
- [x] `.env.example` с 4 токенами
|
||
|
||
### 🔧 SpotBugs (все реальные баги исправлены)
|
||
- [x] FS (7): `\n` → `%n` в MetricsCollector, ServerStatsService
|
||
- [x] DMI (6): Random → static final Random в entertainment командах
|
||
- [x] SF (4): missing default → добавлены default в switch
|
||
- [x] RCN (3): Redundant nullcheck удалены
|
||
- [x] RV (1): Bad abs(hashCode) исправлен
|
||
- [x] Dm (2): Стандартная кодировка → UTF-8
|
||
- [x] OBL/ODR: Database.prepare() закрытие connection
|
||
- [x] REC: RankCardGenerator — узкий catch
|
||
- [x] DLS: FishingService — dead store удалён
|
||
- [x] EI2/EI (96): excluded как DI false positives
|
||
- [x] `ignoreFailures = false`
|
||
|
||
### 🔍 Checkstyle
|
||
- [x] Все 9 нарушений исправлены
|
||
- [x] `maxErrors`/`maxWarnings` удалены (0 нарушений)
|
||
|
||
### 🧪 Тесты
|
||
- [x] BalanceCommandTest (3 теста)
|
||
- [x] DailyCommandTest (2 теста)
|
||
- [x] PayCommandTest (3 теста)
|
||
- [x] CoinCommandTest (2 теста)
|
||
- [x] DiceCommandTest (2 теста)
|
||
- [x] EightBallCommandTest (2 теста)
|
||
- [x] Всего: 51 файл тестов, ~421+ тестов
|
||
|
||
### 🚀 CI/CD
|
||
- [x] Docker publish workflow (ghcr.io)
|
||
- [x] Release workflow (GitHub Releases + JAR artifact)
|