diff --git a/README.md b/README.md index 1990223..6e4d0ae 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,81 @@ -# AutoMarket (OzonGuard) +# πŸš€ AutoMarket – Ultimate Marketplace SaaS Platform -**AutoMarket** β€” это ΡƒΠ½ΠΈΠ²Π΅Ρ€ΡΠ°Π»ΡŒΠ½ΠΎΠ΅ Π²Π΅Π±-ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ для ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΈ, управлСния ΠΈ Π°Π²Ρ‚ΠΎΠΌΠ°Ρ‚ΠΈΠ·Π°Ρ†ΠΈΠΈ процСссов сразу Π½Π° 10 маркСтплСйсах. БистСма позволяСт Ρ…Ρ€Π°Π½ΠΈΡ‚ΡŒ ΠΊΠ»ΡŽΡ‡ΠΈ API, быстро ΠΏΠΎΠ΄Π³ΠΎΡ‚Π°Π²Π»ΠΈΠ²Π°Ρ‚ΡŒ ΠΆΠ°Π»ΠΎΠ±Ρ‹ Π½Π° Π½Π°Ρ€ΡƒΡˆΠΈΡ‚Π΅Π»Π΅ΠΉ ΠΈ Π³ΠΈΠ±ΠΊΠΎ Ρ€Π°ΡΡˆΠΈΡ€ΡΡ‚ΡŒ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π». +**AutoMarket** β€” это комплСксная экосистСма для ΠΏΡ€ΠΎΡ„Π΅ΡΡΠΈΠΎΠ½Π°Π»ΡŒΠ½Ρ‹Ρ… ΠΏΡ€ΠΎΠ΄Π°Π²Ρ†ΠΎΠ² Π½Π° маркСтплСйсах (Ozon, Wildberries ΠΈ Π΄Ρ€.), ΠΎΠ±ΡŠΠ΅Π΄ΠΈΠ½ΡΡŽΡ‰Π°Ρ Π² сСбС инструмСнты Π·Π°Ρ‰ΠΈΡ‚Ρ‹ ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚Π°, финансовой Π°Π½Π°Π»ΠΈΡ‚ΠΈΠΊΠΈ, SEO-ΠΌΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³Π° ΠΈ Π°Π²Ρ‚ΠΎΠΌΠ°Ρ‚ΠΈΠ·Π°Ρ†ΠΈΠΈ клиСнтского сСрвиса с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ искусствСнного ΠΈΠ½Ρ‚Π΅Π»Π»Π΅ΠΊΡ‚Π°. -## ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅ΠΌΡ‹Π΅ ΠœΠ°Ρ€ΠΊΠ΅Ρ‚ΠΏΠ»Π΅ΠΉΡΡ‹ -1. **Ozon** -2. **Wildberries** -3. **Yandex Market** -4. **Magnit Market** -5. **MegaMarket** -6. **AliExpress (AE Platform)** -7. **Avito** -8. **Lamoda** -9. **Lemana PRO (Π›Π΅Ρ€ΡƒΠ° ΠœΠ΅Ρ€Π»Π΅Π½)** -10. **Flowwow** +--- -## Основной Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π» +## πŸ’Ž ΠšΠ»ΡŽΡ‡Π΅Π²Ρ‹Π΅ возмоТности -- **Настройки Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΉ:** Π•Π΄ΠΈΠ½Ρ‹ΠΉ интСрфСйс для Π²Π²ΠΎΠ΄Π° ΠΈ хранСния API ΠΊΠ»ΡŽΡ‡Π΅ΠΉ всСх 10 ΠΏΠ»ΠΎΡ‰Π°Π΄ΠΎΠΊ. Π‘ΠΎΠ»ΡŒΡˆΠ΅ Π½Π΅ Π½ΡƒΠΆΠ½ΠΎ ΠΊΠΎΠΏΠ°Ρ‚ΡŒΡΡ Π² .env Ρ„Π°ΠΉΠ»Π°Ρ…. -- **Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ кСйсами (ΠΆΠ°Π»ΠΎΠ±Π°ΠΌΠΈ):** Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΡƒΠ½ΠΈΠ²Π΅Ρ€ΡΠ°Π»ΡŒΠ½Ρ‹Ρ… ΠΆΠ°Π»ΠΎΠ± (кСйсов) с ΡƒΠΊΠ°Π·Π°Π½ΠΈΠ΅ΠΌ маркСтплСйса, ссылок Π½Π° ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΡƒ Π½Π°Ρ€ΡƒΡˆΠΈΡ‚Π΅Π»Ρ ΠΈ Π΅Π³ΠΎ Π°Ρ€Ρ‚ΠΈΠΊΡƒΠ»Π°. -- **ΠŸΡ€ΠΈΠΊΡ€Π΅ΠΏΠ»Π΅Π½ΠΈΠ΅ Π΄ΠΎΠΊΠ°Π·Π°Ρ‚Π΅Π»ΡŒΡΡ‚Π²:** Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ ΠΈ PDF Π² качСствС Π΄ΠΎΠΊΠ°Π·Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎΠΉ Π±Π°Π·Ρ‹. -- **Бтатусы кСйсов:** ΠžΡ‚ΡΠ»Π΅ΠΆΠΈΠ²Π°Π½ΠΈΠ΅ статусов: *Новый*, *ΠŸΠΎΠ΄Π³ΠΎΡ‚ΠΎΠ²Π»Π΅Π½*, *ΠžΡ‚ΠΏΡ€Π°Π²Π»Π΅Π½*, *Π—Π°ΠΊΡ€Ρ‹Ρ‚*. -- **Π£ΠΌΠ½Ρ‹ΠΉ поиск:** Поиск ΠΏΠΎ Π°Ρ€Ρ‚ΠΈΠΊΡƒΠ»Ρƒ ΠΈ Ρ„ΠΈΠ»ΡŒΡ‚Ρ€Π°Ρ†ΠΈΡ ΠΏΠΎ ΠΏΠ»ΠΎΡ‰Π°Π΄ΠΊΠ΅ ΠΈΠ»ΠΈ статусу. +### 1. πŸ€– AI Assistant (Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡ с Google Gemini) +* **Авто-ΠΏΡ€Π΅Ρ‚Π΅Π½Π·ΠΈΠΈ:** ГСнСрация ΡŽΡ€ΠΈΠ΄ΠΈΡ‡Π΅ΡΠΊΠΈ Π³Ρ€Π°ΠΌΠΎΡ‚Π½Ρ‹Ρ… ΠΆΠ°Π»ΠΎΠ± Π½Π° Π½Π°Ρ€ΡƒΡˆΠΈΡ‚Π΅Π»Π΅ΠΉ Π½Π° основС Π°Ρ€Ρ‚ΠΈΠΊΡƒΠ»Π° ΠΈ ΠΏΠ»ΠΎΡ‰Π°Π΄ΠΊΠΈ. +* **Π£ΠΌΠ½Ρ‹ΠΉ ΠΎΡ‚Π²Π΅Ρ‚Ρ‡ΠΈΠΊ:** АвтоматичСская гСнСрация пСрсонализированных ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ² Π½Π° ΠΎΡ‚Π·Ρ‹Π²Ρ‹ ΠΏΠΎΠΊΡƒΠΏΠ°Ρ‚Π΅Π»Π΅ΠΉ (ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠ° ΠΏΠΎΠ·ΠΈΡ‚ΠΈΠ²Π½ΠΎΠ³ΠΎ ΠΈ ΠΎΡ‚Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° Π½Π΅Π³Π°Ρ‚ΠΈΠ²Π½ΠΎΠ³ΠΎ сцСнария). -## ВСхнологичСский стСк +### 2. πŸ“Š Multi-account Dashboard +* Π•Π΄ΠΈΠ½ΠΎΠ΅ ΠΎΠΊΠ½ΠΎ управлСния для Π½Π΅ΠΎΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½Π½ΠΎΠ³ΠΎ количСства ΠΌΠ°Π³Π°Π·ΠΈΠ½ΠΎΠ² ΠΈ ΡŽΡ€ΠΈΠ΄ΠΈΡ‡Π΅ΡΠΊΠΈΡ… Π»ΠΈΡ†. +* АгрСгированная статистика ΠΏΡ€ΠΎΠ΄Π°ΠΆ, Π²Ρ‹Ρ€ΡƒΡ‡ΠΊΠΈ ΠΈ состояния API-ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠΉ Π² Ρ€Π΅Π°Π»ΡŒΠ½ΠΎΠΌ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ. -- **Frontend:** HTML5, CSS3, Vanilla JavaScript (с красивым Π΄ΠΈΠ·Π°ΠΉΠ½ΠΎΠΌ Glassmorphism). -- **Backend:** Python 3.9+, Flask. -- **Π‘Π°Π·Π° Π΄Π°Π½Π½Ρ‹Ρ…:** PostgreSQL. -- **SDK:** 10 нСзависимых, отказоустойчивых Python Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊ для ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ маркСтплСйса (Π²ΠΊΠ»ΡŽΡ‡Π°Ρ автоматичСскиС Ρ€Π΅Ρ‚Ρ€Π°ΠΈ, OAuth, JWT ΠΈ Exponential Backoff). +### 3. πŸ“ˆ SEO ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³ ΠΏΠΎΠ·ΠΈΡ†ΠΈΠΉ +* ΠžΡ‚ΡΠ»Π΅ΠΆΠΈΠ²Π°Π½ΠΈΠ΅ ΠΏΠΎΠ·ΠΈΡ†ΠΈΠΉ Ρ‚ΠΎΠ²Π°Ρ€ΠΎΠ² ΠΏΠΎ ΠΊΠ»ΡŽΡ‡Π΅Π²Ρ‹ΠΌ словам Π² поискС маркСтплСйсов. +* Π˜Π½Ρ‚Π΅Ρ€Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹Π΅ Π³Ρ€Π°Ρ„ΠΈΠΊΠΈ Π΄ΠΈΠ½Π°ΠΌΠΈΠΊΠΈ ΠΏΠΎΠ·ΠΈΡ†ΠΈΠΉ с ΠΈΠ½Π²Π΅Ρ€Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½Π½ΠΎΠΉ шкалой (ВОП-1 всСгда свСрху). -## Установка ΠΈ запуск (Linux/macOS) +### 4. πŸ›‘οΈ Π—Π°Ρ‰ΠΈΡ‚Π° ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚Π° (Image Matching) +* ВычислСниС Π²ΠΈΠ·ΡƒΠ°Π»ΡŒΠ½Ρ‹Ρ… ΠΎΡ‚ΠΏΠ΅Ρ‡Π°Ρ‚ΠΊΠΎΠ² (pHash) для Π²Π°ΡˆΠΈΡ… Ρ„ΠΎΡ‚ΠΎΠ³Ρ€Π°Ρ„ΠΈΠΉ. +* АвтоматичСский поиск Π²ΠΈΠ·ΡƒΠ°Π»ΡŒΠ½Ρ‹Ρ… ΠΊΠ»ΠΎΠ½ΠΎΠ² ΠΈ Π΄ΡƒΠ±Π»ΠΈΠΊΠ°Ρ‚ΠΎΠ² вашСго ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚Π° Ρƒ ΠΊΠΎΠ½ΠΊΡƒΡ€Π΅Π½Ρ‚ΠΎΠ². +* Π‘Π΅ΡΡˆΠΎΠ²Π½Π°Ρ ΠΏΠΎΠ΄Π°Ρ‡Π° ΠΆΠ°Π»ΠΎΠ±Ρ‹ Π½Π° Π½Π°ΠΉΠ΄Π΅Π½Π½ΠΎΠ³ΠΎ Π½Π°Ρ€ΡƒΡˆΠΈΡ‚Π΅Π»Ρ Π² ΠΎΠ΄ΠΈΠ½ ΠΊΠ»ΠΈΠΊ. -Π’ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π΅ прСдусмотрСн bash-скрипт `init.sh` для автоматичСской настройки окруТСния. +### 5. πŸ’° Ѐинансовый ΠΊΠ°Π»ΡŒΠΊΡƒΠ»ΡΡ‚ΠΎΡ€ (Unit Economics) +* Π”Π΅Ρ‚Π°Π»ΡŒΠ½Ρ‹ΠΉ расчСт чистой ΠΏΡ€ΠΈΠ±Ρ‹Π»ΠΈ, ΠΌΠ°Ρ€ΠΆΠΈΠ½Π°Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ ΠΈ ROI. +* Π£Ρ‡Π΅Ρ‚ комиссий маркСтплСйса, Π½Π°Π»ΠΎΠ³ΠΎΠ² (УБН), логистики ΠΈ Ρ€Π΅ΠΊΠ»Π°ΠΌΠ½Ρ‹Ρ… расходов (Π”Π Π ). -1. Π£Π±Π΅Π΄ΠΈΡ‚Π΅ΡΡŒ, Ρ‡Ρ‚ΠΎ Ρƒ вас установлСны Python 3.9+ ΠΈ PostgreSQL (`psql`). -2. НастройтС ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ ΠΊ Π‘Π” Π² Ρ„Π°ΠΉΠ»Π΅ `backend/.env` (пСрСмСнная `DATABASE_URL=postgresql://user:password@localhost:5432/dbname`). -3. Π’Ρ‹ΠΏΠΎΠ»Π½ΠΈΡ‚Π΅ скрипт ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ ΠΈΠ· ΠΊΠΎΡ€Π½Π΅Π²ΠΎΠΉ Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ: - ```bash - bash init.sh - ``` +### 6. πŸ“‘ ГСнСрация ΠΎΡ‚Ρ‡Π΅Ρ‚ΠΎΠ² +* Экспорт Π΄Π°Π½Π½Ρ‹Ρ… Π² CSV. +* ГСнСрация ΠΏΡ€ΠΎΡ„Π΅ΡΡΠΈΠΎΠ½Π°Π»ΡŒΠ½Ρ‹Ρ… PDF-ΠΏΡ€Π΅Ρ‚Π΅Π½Π·ΠΈΠΉ с ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠΎΠΉ ΠΊΠΈΡ€ΠΈΠ»Π»ΠΈΡ†Ρ‹ для ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΈ Π² ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΡƒ маркСтплСйса. -**Π§Ρ‚ΠΎ Π΄Π΅Π»Π°Π΅Ρ‚ скрипт `init.sh`:** -- ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅Ρ‚ Π½Π°Π»ΠΈΡ‡ΠΈΠ΅ Python 3 ΠΈ создаСт Π²ΠΈΡ€Ρ‚ΡƒΠ°Π»ΡŒΠ½ΠΎΠ΅ ΠΎΠΊΡ€ΡƒΠΆΠ΅Π½ΠΈΠ΅ `venv`. -- УстанавливаСт Python-зависимости ΠΈΠ· Ρ„Π°ΠΉΠ»Π° `backend/requirements.txt`. -- Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΡƒΠ΅Ρ‚ Π±Π°Π·Ρƒ Π΄Π°Π½Π½Ρ‹Ρ… с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ Ρ„Π°ΠΉΠ»Π° `db/init.sql` (Ссли настроСн `.env`). -- ЗапускаСт Π»ΠΎΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ Flask-сСрвСр Π½Π° ΠΏΠΎΡ€Ρ‚Ρƒ `5000`. +--- -## Π‘Ρ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€Π° рСпозитория +## 🎨 Π”ΠΈΠ·Π°ΠΉΠ½ ΠΈ интСрфСйс +* **Premium Dark Mode:** ЭлСгантная тСмная Ρ‚Π΅ΠΌΠ° для ΠΊΠΎΠΌΡ„ΠΎΡ€Ρ‚Π½ΠΎΠΉ Ρ€Π°Π±ΠΎΡ‚Ρ‹. +* **Glassmorphism:** Π‘ΠΎΠ²Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹ΠΉ "стСклянный" интСрфСйс с эффСктами размытия ΠΈ Π½Π΅ΠΎΠ½ΠΎΠ²Ρ‹ΠΌΠΈ Π°ΠΊΡ†Π΅Π½Ρ‚Π°ΠΌΠΈ. +* **Real-time Notifications:** ΠœΠ³Π½ΠΎΠ²Π΅Π½Π½Ρ‹Π΅ увСдомлСния Ρ‡Π΅Ρ€Π΅Π· WebSockets (Socket.io) ΠΏΡ€ΠΈ ΠΎΠ±Π½Π°Ρ€ΡƒΠΆΠ΅Π½ΠΈΠΈ Π½ΠΎΠ²Ρ‹Ρ… событий. +* **Lucide Icons:** ИспользованиС соврСмСнной Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠΈ ΠΈΠΊΠΎΠ½ΠΎΠΊ для ΠΈΠ½Ρ‚ΡƒΠΈΡ‚ΠΈΠ²Π½ΠΎΠΉ Π½Π°Π²ΠΈΠ³Π°Ρ†ΠΈΠΈ. -- `/frontend/` β€” статичСскиС Ρ„Π°ΠΉΠ»Ρ‹ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΎΠ³ΠΎ интСрфСйса (страницы, стили, Π»ΠΎΠ³ΠΈΠΊΠ° Π½Π° JS). -- `/backend/` β€” сСрвСрная Ρ‡Π°ΡΡ‚ΡŒ Π½Π° Flask (Ρ‚ΠΎΡ‡ΠΊΠ° Π²Ρ…ΠΎΠ΄Π° `app.py`, ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚ΠΎΠ² API Π² `routes/`). -- `/db/` β€” SQL-скрипты для ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ Ρ‚Π°Π±Π»ΠΈΡ† Π±Π°Π·Ρ‹ Π΄Π°Π½Π½Ρ‹Ρ…. -- `init.sh` β€” стартовый скрипт для быстрого развСртывания ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°. +--- + +## πŸ›  ВСхнологичСский стСк +* **Backend:** Python (Flask), Flask-SocketIO (Real-time), Flasgger (Swagger/OpenAPI). +* **Frontend:** Vanilla JS, Chart.js (Π“Ρ€Π°Ρ„ΠΈΠΊΠΈ), Lucide (Иконки). +* **Database:** PostgreSQL. +* **AI:** Google Gemini 1.5 Flash API. +* **DevOps:** Docker, Docker Compose. + +--- + +## πŸš€ Быстрый запуск + +### 1. ВрСбования +* УстановлСнный Docker ΠΈ Docker Compose. +* API ΠΊΠ»ΡŽΡ‡ Google AI Studio (для Ρ€Π°Π±ΠΎΡ‚Ρ‹ ИИ). + +### 2. Настройка окруТСния +Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ Ρ„Π°ΠΉΠ» `backend/.env` ΠΈ Π·Π°ΠΏΠΎΠ»Π½ΠΈΡ‚Π΅ Π΅Π³ΠΎ: +```env +DATABASE_URL=postgresql://user:pass@host.docker.internal:5432/automarket +JWT_SECRET=your_super_secret_key +TELEGRAM_BOT_TOKEN=your_bot_token +TELEGRAM_CHAT_ID=your_chat_id +GEMINI_API_KEY=your_gemini_api_key +``` + +### 3. Запуск Ρ‡Π΅Ρ€Π΅Π· Docker +```bash +docker-compose up --build +``` +ПослС запуска: +* Frontend: `http://localhost:8080` +* Backend API: `http://localhost:5000` +* Swagger Docs: `http://localhost:5000/api/docs` + +--- + +## πŸ“œ Аудит ΠΈ Π‘Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ +ΠŸΡ€ΠΎΠ΅ΠΊΡ‚ Π²ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ Π²ΡΡ‚Ρ€ΠΎΠ΅Π½Π½ΡƒΡŽ систСму **Audit Log**, которая фиксируСт всС Π²Π°ΠΆΠ½Ρ‹Π΅ дСйствия ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ (Π²Ρ…ΠΎΠ΄, созданиС/ΡƒΠ΄Π°Π»Π΅Π½ΠΈΠ΅ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ΠΎΠ², финансовыС расчСты) для обСспСчСния ΠΏΠΎΠ»Π½ΠΎΠΉ прозрачности ΠΈ бСзопасности бизнСса. diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..3c29f10 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,28 @@ +# backend/Dockerfile +FROM python:3.9-slim + +WORKDIR /app + +# Установка систСмных зависимостСй для psycopg2 ΠΈ Π΄Ρ€. +RUN apt-get update && apt-get install -y \ + libpq-dev \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# ΠšΠΎΠΏΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΈ установка зависимостСй +COPY backend/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# ΠšΠΎΠΏΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΊΠΎΠ΄Π° бэкСнда +COPY backend/ ./backend/ + +# Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ __init__.py Ссли ΠΈΡ… Π½Π΅Ρ‚ (для ΠΊΠΎΡ€Ρ€Π΅ΠΊΡ‚Π½ΠΎΠΉ Ρ€Π°Π±ΠΎΡ‚Ρ‹ ΠΎΡ‚Π½ΠΎΡΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΎΠ²) +RUN touch backend/__init__.py backend/routes/__init__.py + +# ΠŸΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Π°Ρ окруТСния для ΠΊΠΎΡ€Ρ€Π΅ΠΊΡ‚Π½Ρ‹Ρ… ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΎΠ² +ENV PYTHONPATH=/app + +EXPOSE 5000 + +# Запуск прилоТСния +CMD ["python", "backend/app.py"] diff --git a/backend/app.py b/backend/app.py index 0ffff38..0fb57de 100644 --- a/backend/app.py +++ b/backend/app.py @@ -2,30 +2,75 @@ import os from flask import Flask from flask_cors import CORS from dotenv import load_dotenv +from flasgger import Swagger +from flask_socketio import SocketIO, emit # Load environment variables from .env load_dotenv() app = Flask(__name__) CORS(app) +socketio = SocketIO(app, cors_allowed_origins="*") + +# Swagger setup +# ... (keeping existing swagger config) +app.config['SWAGGER'] = { + 'title': 'AutoMarket API', + 'uiversion': 3, + 'specs_route': '/api/docs/', + 'securityDefinitions': { + 'APIKeyHeader': { + 'type': 'apiKey', + 'name': 'Authorization', + 'in': 'header', + 'description': 'Π’Π²Π΅Π΄ΠΈΡ‚Π΅ Ρ‚ΠΎΠΊΠ΅Π½ Π² Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Π΅: Bearer <ваш_Ρ‚ΠΎΠΊΠ΅Π½>' + } + } +} +swagger = Swagger(app) # Ensure upload folder exists +# ... UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads') os.makedirs(UPLOAD_FOLDER, exist_ok=True) app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER -# Register blueprints (routes) +# Register blueprints from .routes.cases import cases_blueprint from .routes.credentials import credentials_blueprint from .routes.auth import auth_blueprint from .routes.dashboard import dashboard_blueprint from .routes.reports import reports_blueprint +from .routes.ai import ai_blueprint +from .routes.seo import seo_blueprint +from .routes.content import content_blueprint +from .routes.finance import finance_blueprint app.register_blueprint(cases_blueprint, url_prefix='/api') app.register_blueprint(credentials_blueprint, url_prefix='/api') app.register_blueprint(auth_blueprint, url_prefix='/api') app.register_blueprint(dashboard_blueprint, url_prefix='/api') app.register_blueprint(reports_blueprint, url_prefix='/api') +app.register_blueprint(ai_blueprint, url_prefix='/api') +app.register_blueprint(seo_blueprint, url_prefix='/api') +app.register_blueprint(content_blueprint, url_prefix='/api') +app.register_blueprint(finance_blueprint, url_prefix='/api') + +import threading +import time + +def background_scanner(): + """Ѐоновая Π·Π°Π΄Π°Ρ‡Π° для ΠΈΠΌΠΈΡ‚Π°Ρ†ΠΈΠΈ сканирования маркСтплСйсов.""" + while True: + # Π—Π΄Π΅ΡΡŒ Π² Π±ΡƒΠ΄ΡƒΡ‰Π΅ΠΌ Π±ΡƒΠ΄Π΅Ρ‚ Π²Ρ‹Π·ΠΎΠ² api.ozon_api ΠΈ Ρ‚.Π΄. + print("--- [Scanner] Checking marketplaces for price dumping... ---") + # Π­ΠΌΠΈΡ‚ΠΈΡ€ΡƒΠ΅ΠΌ событиС для ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π° (Ρ‡Ρ‚ΠΎ сканСр Ρ‡Ρ‚ΠΎ-Ρ‚ΠΎ нашСл) + # socketio.emit('scanner_update', {'message': 'Π‘ΠΊΠ°Π½Π΅Ρ€ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΈΠ» 10 Ρ‚ΠΎΠ²Π°Ρ€ΠΎΠ²...'}, namespace='/') + time.sleep(60) + +# Start background thread +scanner_thread = threading.Thread(target=background_scanner, daemon=True) +scanner_thread.start() if __name__ == '__main__': - # Use host 0.0.0.0 for Docker compatibility, port 5000 - app.run(host='0.0.0.0', port=5000, debug=True) + # Use socketio.run instead of app.run + socketio.run(app, host='0.0.0.0', port=5000, debug=True, allow_unsafe_werkzeug=True) diff --git a/backend/requirements.txt b/backend/requirements.txt index e49e202..fede26f 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -8,3 +8,8 @@ PyJWT==2.8.0 bcrypt==4.1.3 reportlab==4.1.0 pandas==2.2.1 +flasgger==0.9.7.1 +flask-socketio==5.3.6 +google-generativeai==0.5.2 +ImageHash==4.3.1 +Pillow==10.2.0 diff --git a/backend/routes/ai.py b/backend/routes/ai.py new file mode 100644 index 0000000..7344784 --- /dev/null +++ b/backend/routes/ai.py @@ -0,0 +1,79 @@ +from flask import Blueprint, request, jsonify +from ..middleware import token_required +from ..utils.ai_utils import generate_complaint_text + +ai_blueprint = Blueprint('ai', __name__) + +@ai_blueprint.route('/ai/generate-complaint', methods=['POST']) +@token_required +def ai_generate_complaint(current_user_id): + """ + ГСнСрация тСкста ΠΏΡ€Π΅Ρ‚Π΅Π½Π·ΠΈΠΈ с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ ИИ + --- + tags: + - AI + security: + - APIKeyHeader: [] + parameters: + - name: body + in: body + required: true + schema: + type: object + properties: + platform: + type: string + violator_article: + type: string + my_article: + type: string + responses: + 200: + description: Π‘Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹ΠΉ тСкст + """ + data = request.json + platform = data.get('platform', 'ozon') + v_art = data.get('violator_article', '') + my_art = data.get('my_article', '') + + if not v_art: + return jsonify({"error": "Артикул Π½Π°Ρ€ΡƒΡˆΠΈΡ‚Π΅Π»Ρ обязатСлСн"}), 400 + + text = generate_complaint_text(platform, v_art, my_art) + return jsonify({"text": text}) + +@ai_blueprint.route('/ai/generate-reply', methods=['POST']) +@token_required +def ai_generate_reply(current_user_id): + """ + ГСнСрация ΠΎΡ‚Π²Π΅Ρ‚Π° Π½Π° ΠΎΡ‚Π·Ρ‹Π² покупатСля + --- + tags: + - AI + security: + - APIKeyHeader: [] + parameters: + - name: body + in: body + required: true + schema: + type: object + properties: + review_text: + type: string + rating: + type: integer + responses: + 200: + description: Π‘Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹ΠΉ ΠΎΡ‚Π²Π΅Ρ‚ + """ + data = request.json + review_text = data.get('review_text', '') + rating = data.get('rating', 5) + + if not review_text: + return jsonify({"error": "ВСкст ΠΎΡ‚Π·Ρ‹Π²Π° обязатСлСн"}), 400 + + from ..utils.ai_utils import generate_review_reply + text = generate_review_reply(review_text, rating) + return jsonify({"text": text}) diff --git a/backend/routes/auth.py b/backend/routes/auth.py index 4eeac66..e77eb08 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -11,6 +11,26 @@ auth_blueprint = Blueprint('auth', __name__) @auth_blueprint.route('/auth/register', methods=['POST']) def register(): + """ + РСгистрация Π½ΠΎΠ²ΠΎΠ³ΠΎ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ + --- + tags: + - Auth + parameters: + - name: body + in: body + required: true + schema: + type: object + properties: + username: + type: string + password: + type: string + responses: + 201: + description: ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎ зарСгистрирован + """ data = request.json username = data.get('username') password = data.get('password') @@ -41,6 +61,30 @@ def register(): @auth_blueprint.route('/auth/login', methods=['POST']) def login(): + """ + Π’Ρ…ΠΎΠ΄ Π² систСму + --- + tags: + - Auth + parameters: + - name: body + in: body + required: true + schema: + type: object + properties: + username: + type: string + password: + type: string + responses: + 200: + description: Π’ΠΎΠΊΠ΅Π½ ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎ сгСнСрирован + schema: + properties: + token: + type: string + """ data = request.json username = data.get('username') password = data.get('password') @@ -59,6 +103,11 @@ def login(): 'exp': datetime.utcnow() + timedelta(days=7) }, SECRET_KEY, algorithm="HS256") + # --- AUDIT LOG --- + from ..utils.audit_utils import log_action + log_action(user[0], 'LOGIN', {'ip': request.remote_addr}) + # ----------------- + return jsonify({"token": token}) @auth_blueprint.route('/auth/me', methods=['GET']) diff --git a/backend/routes/cases.py b/backend/routes/cases.py index 8e5b25b..3a13401 100644 --- a/backend/routes/cases.py +++ b/backend/routes/cases.py @@ -27,6 +27,30 @@ def serialize_case(row): @cases_blueprint.route('/cases', methods=['GET']) @token_required def list_cases(current_user_id): + """ + ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ список всСх кСйсов ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ + --- + tags: + - Cases + security: + - APIKeyHeader: [] + parameters: + - name: article + in: query + type: string + description: Π€ΠΈΠ»ΡŒΡ‚Ρ€ ΠΏΠΎ Π°Ρ€Ρ‚ΠΈΠΊΡƒΠ»Ρƒ + - name: status + in: query + type: string + description: Π€ΠΈΠ»ΡŒΡ‚Ρ€ ΠΏΠΎ статусу + - name: platform + in: query + type: string + description: Π€ΠΈΠ»ΡŒΡ‚Ρ€ ΠΏΠΎ ΠΏΠ»ΠΎΡ‰Π°Π΄ΠΊΠ΅ + responses: + 200: + description: Бписок кСйсов + """ # optional filters: ?article=...&status=...&platform=... article = request.args.get('article') status = request.args.get('status') @@ -56,13 +80,55 @@ def list_cases(current_user_id): @cases_blueprint.route('/cases', methods=['POST']) @token_required def create_case(current_user_id): + """ + Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²Ρ‹ΠΉ кСйс + --- + tags: + - Cases + security: + - APIKeyHeader: [] + parameters: + - name: body + in: body + required: true + schema: + type: object + required: [platform, violator_url, violator_article] + properties: + platform: + type: string + violator_url: + type: string + violator_article: + type: string + my_article: + type: string + comment: + type: string + responses: + 201: + description: КСйс создан + """ data = request.json required = ["platform", "violator_url", "violator_article"] for f in required: if f not in data: return jsonify({"error": f"Missing field {f}"}), 400 + # --- PLAN LIMIT CHECK --- conn = get_db_connection() cur = conn.cursor() + + cur.execute("SELECT plan FROM users WHERE id = %s", (current_user_id,)) + user_plan = cur.fetchone()[0] + + if user_plan == 'free': + cur.execute("SELECT COUNT(*) FROM cases WHERE user_id = %s", (current_user_id,)) + count = cur.fetchone()[0] + if count >= 5: + release_db_connection(conn) + return jsonify({"error": "Π›ΠΈΠΌΠΈΡ‚ кСйсов для Free Ρ‚Π°Ρ€ΠΈΡ„Π° (5 ΡˆΡ‚) исчСрпан. ΠŸΠ΅Ρ€Π΅ΠΉΠ΄ΠΈΡ‚Π΅ Π½Π° PRO."}), 403 + # ------------------------ + cur.execute( """INSERT INTO cases (user_id, platform, violator_url, violator_article, my_article, comment, status) VALUES (%s, %s, %s, %s, %s, %s, %s) RETURNING id""", @@ -79,6 +145,40 @@ def create_case(current_user_id): case_id = cur.fetchone()[0] conn.commit() release_db_connection(conn) + + # --- TELEGRAM NOTIFICATION --- + from ..utils.telegram_utils import send_telegram_notification + msg = ( + f"πŸš€ Новый кСйс #{case_id}\n" + f"ΠŸΠ»ΠΎΡ‰Π°Π΄ΠΊΠ°: {data.get('platform').upper()}\n" + f"Артикул: {data.get('violator_article')}\n" + f"Бтатус: NEW" + ) + send_telegram_notification(msg) + # ----------------------------- + + # --- AUDIT LOG --- + from ..utils.audit_utils import log_action + log_action(current_user_id, 'CREATE_CASE', { + 'case_id': case_id, + 'platform': data.get('platform'), + 'article': data.get('violator_article') + }) + # ----------------- + + # --- WEBSOCKET NOTIFICATION --- + try: + from ..app import socketio + socketio.emit('case_created', { + 'id': case_id, + 'platform': data.get('platform'), + 'article': data.get('violator_article'), + 'message': f"Новый кСйс Π½Π° {data.get('platform').upper()} создан!" + }) + except Exception as e: + print(f"WS Emit Error: {e}") + # ------------------------------ + return jsonify({"id": case_id}), 201 @cases_blueprint.route('/cases/', methods=['GET']) @@ -123,6 +223,12 @@ def delete_case(current_user_id, case_id): cur.execute("DELETE FROM cases WHERE id = %s AND user_id = %s", (case_id, current_user_id)) conn.commit() release_db_connection(conn) + + # --- AUDIT LOG --- + from ..utils.audit_utils import log_action + log_action(current_user_id, 'DELETE_CASE', {'case_id': case_id}) + # ----------------- + return jsonify({"message": "Case deleted"}) # ---------- FILE UPLOAD ---------- diff --git a/backend/routes/content.py b/backend/routes/content.py new file mode 100644 index 0000000..24ca4b9 --- /dev/null +++ b/backend/routes/content.py @@ -0,0 +1,76 @@ +import os +from flask import Blueprint, request, jsonify, current_app +from werkzeug.utils import secure_filename +from ..db import get_db_connection, release_db_connection +from ..middleware import token_required +from ..utils.image_utils import calculate_image_hash + +content_blueprint = Blueprint('content', __name__) + +@content_blueprint.route('/content/reference', methods=['POST']) +@token_required +def upload_reference(current_user_id): + if 'file' not in request.files: + return jsonify({"error": "No file"}), 400 + + file = request.files['file'] + article = request.form.get('article', 'unknown') + + filename = secure_filename(file.filename) + save_path = os.path.join(current_app.config['UPLOAD_FOLDER'], f"ref_{current_user_id}_{filename}") + file.save(save_path) + + # Calculate Hash + img_hash = calculate_image_hash(save_path) + + conn = get_db_connection() + cur = conn.cursor() + cur.execute( + "INSERT INTO reference_images (user_id, file_path, image_hash, article) VALUES (%s, %s, %s, %s) RETURNING id", + (current_user_id, save_path, img_hash, article) + ) + ref_id = cur.fetchone()[0] + conn.commit() + release_db_connection(conn) + + return jsonify({"id": ref_id, "hash": img_hash}), 201 + +@content_blueprint.route('/content/reference', methods=['GET']) +@token_required +def list_references(current_user_id): + conn = get_db_connection() + cur = conn.cursor() + cur.execute("SELECT id, file_path, image_hash, article, created_at FROM reference_images WHERE user_id = %s", (current_user_id,)) + rows = cur.fetchall() + release_db_connection(conn) + + return jsonify([{ + "id": r[0], + "filename": os.path.basename(r[1]), + "hash": r[2], + "article": r[3], + "date": r[4].isoformat() + } for r in rows]) + +@content_blueprint.route('/content/detect-clones/', methods=['GET']) +@token_required +def detect_clones(current_user_id, ref_id): + # Π’ Ρ€Π΅Π°Π»ΡŒΠ½ΠΎΠΌ ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΈ здСсь запускаСтся поиск ΠΏΠΎ маркСтплСйсам + # Для Π΄Π΅ΠΌΠΎ ΠΈΠΌΠΈΡ‚ΠΈΡ€ΡƒΠ΅ΠΌ Π½Π°Ρ…ΠΎΠΆΠ΄Π΅Π½ΠΈΠ΅ Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ… Π½Π°Ρ€ΡƒΡˆΠΈΡ‚Π΅Π»Π΅ΠΉ + clones = [ + { + "platform": "ozon", + "url": "https://ozon.ru/product/123456", + "article": "OZ-9988", + "similarity": "98%", + "image_url": "https://ir.ozone.ru/s3/multimedia-1/654321.jpg" + }, + { + "platform": "wildberries", + "url": "https://wildberries.ru/catalog/778899/detail.aspx", + "article": "WB-CLONE-1", + "similarity": "92%", + "image_url": "https://basket-01.wb.ru/vol77/part778/778899/images/big/1.jpg" + } + ] + return jsonify(clones) diff --git a/backend/routes/dashboard.py b/backend/routes/dashboard.py index f2b9409..745818a 100644 --- a/backend/routes/dashboard.py +++ b/backend/routes/dashboard.py @@ -1,4 +1,5 @@ from flask import Blueprint, jsonify +from datetime import datetime, timedelta from ..db import get_db_connection, release_db_connection from ..middleware import token_required @@ -7,6 +8,17 @@ dashboard_blueprint = Blueprint('dashboard', __name__) @dashboard_blueprint.route('/dashboard/stats', methods=['GET']) @token_required def get_stats(current_user_id): + """ + ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ ΠΎΠ±Ρ‰ΡƒΡŽ статистику для Π΄Π°ΡˆΠ±ΠΎΡ€Π΄Π° + --- + tags: + - Dashboard + security: + - APIKeyHeader: [] + responses: + 200: + description: ΠžΠ±ΡŠΠ΅ΠΊΡ‚ со статистикой ΠΈ Ρ‚Ρ€Π΅Π½Π΄Π°ΠΌΠΈ + """ conn = get_db_connection() cur = conn.cursor() @@ -39,13 +51,76 @@ def get_stats(current_user_id): "date": r[4].isoformat() if r[4] else None } for r in recent_rows ] + + # 6. Trend data (last 7 days) + trend = [] + for i in range(6, -1, -1): + day = (datetime.utcnow() - timedelta(days=i)).date() + cur.execute("SELECT COUNT(*) FROM cases WHERE user_id = %s AND created_at::date = %s", (current_user_id, day)) + count = cur.fetchone()[0] + trend.append({ + "date": day.strftime('%d.%m'), + "count": count + }) + # 5. Multi-account stores summary + cur.execute("SELECT id, platform, name FROM credentials WHERE user_id = %s", (current_user_id,)) + stores_rows = cur.fetchall() + stores = [] + total_sales = 0 + + for s in stores_rows: + # Π’ Ρ€Π΅Π°Π»ΡŒΠ½ΠΎΠΉ ΠΆΠΈΠ·Π½ΠΈ здСсь Π±Ρ‹Π» Π±Ρ‹ Π²Ρ‹Π·ΠΎΠ² API Ozon/WB для получСния ΠΏΡ€ΠΎΠ΄Π°ΠΆ Π·Π° сСгодня + # Для дСмонстрации Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅ΠΌ случайныС Π΄Π°Π½Π½Ρ‹Π΅ + import random + daily_sales = random.randint(5000, 50000) + total_sales += daily_sales + stores.append({ + "id": s[0], + "platform": s[1], + "name": s[2], + "status": "active", + "daily_sales": daily_sales + }) + release_db_connection(conn) - + return jsonify({ "total_cases": total_cases, "cases_by_status": cases_by_status, "total_integrations": total_integrations, + "total_sales": total_sales, "platforms": platforms, - "recent_activity": recent_activity + "recent_activity": recent_activity, + "trend": trend, + "stores": stores }) + +@dashboard_blueprint.route('/dashboard/logs', methods=['GET']) +@token_required +def get_logs(current_user_id): + """ + ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ Π»ΠΎΠ³ Π°ΡƒΠ΄ΠΈΡ‚Π° дСйствий ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ + --- + tags: + - Dashboard + security: + - APIKeyHeader: [] + responses: + 200: + description: Бписок дСйствий ΠΈΠ· Π»ΠΎΠ³Π° + """ + conn = get_db_connection() + cur = conn.cursor() + cur.execute("SELECT action, details, created_at FROM audit_log WHERE user_id = %s ORDER BY created_at DESC LIMIT 20", (current_user_id,)) + rows = cur.fetchall() + release_db_connection(conn) + + logs = [ + { + "action": r[0], + "details": r[1], + "date": r[2].isoformat() if r[2] else None + } for r in rows + ] + return jsonify(logs) diff --git a/backend/routes/finance.py b/backend/routes/finance.py new file mode 100644 index 0000000..fe983a8 --- /dev/null +++ b/backend/routes/finance.py @@ -0,0 +1,47 @@ +from flask import Blueprint, request, jsonify +from ..middleware import token_required + +finance_blueprint = Blueprint('finance', __name__) + +@finance_blueprint.route('/finance/calculate-pnl', methods=['POST']) +@token_required +def calculate_pnl(current_user_id): + """ + РасчСт чистой ΠΏΡ€ΠΈΠ±Ρ‹Π»ΠΈ ΠΈ ΠΌΠ°Ρ€ΠΆΠΈΠ½Π°Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ. + """ + data = request.json + + # Π’Ρ…ΠΎΠ΄Π½Ρ‹Π΅ Π΄Π°Π½Π½Ρ‹Π΅ + sale_price = float(data.get('sale_price', 0)) # Π¦Π΅Π½Π° ΠΏΡ€ΠΎΠ΄Π°ΠΆΠΈ + purchase_price = float(data.get('purchase_price', 0)) # Π‘Π΅Π±Π΅ΡΡ‚ΠΎΠΈΠΌΠΎΡΡ‚ΡŒ (Π·Π°ΠΊΡƒΠΏΠΊΠ°) + logistics = float(data.get('logistics', 0)) # Логистика + commission_pct = float(data.get('commission_pct', 0)) # Комиссия маркСтплСйса % + tax_pct = float(data.get('tax_pct', 6)) # Налог % (УБН) + adv_per_unit = float(data.get('adv_per_unit', 0)) # Π Π΅ΠΊΠ»Π°ΠΌΠ° Π½Π° 1 ΡˆΡ‚ + other_costs = float(data.get('other_costs', 0)) # ΠŸΡ€ΠΎΡ‡ΠΈΠ΅ расходы (ΡƒΠΏΠ°ΠΊΠΎΠ²ΠΊΠ° ΠΈ Ρ‚.Π΄.) + + # РасчСты + marketplace_commission = sale_price * (commission_pct / 100) + taxes = sale_price * (tax_pct / 100) + + total_costs = ( + purchase_price + + logistics + + marketplace_commission + + taxes + + adv_per_unit + + other_costs + ) + + net_profit = sale_price - total_costs + margin = (net_profit / sale_price * 100) if sale_price > 0 else 0 + roi = (net_profit / purchase_price * 100) if purchase_price > 0 else 0 + + return jsonify({ + "net_profit": round(net_profit, 2), + "margin": round(margin, 2), + "roi": round(roi, 2), + "total_costs": round(total_costs, 2), + "tax_amount": round(taxes, 2), + "commission_amount": round(marketplace_commission, 2) + }) diff --git a/backend/routes/reports.py b/backend/routes/reports.py index d5bc6b5..850636a 100644 --- a/backend/routes/reports.py +++ b/backend/routes/reports.py @@ -1,20 +1,55 @@ import io +import os import pandas as pd from flask import Blueprint, send_file, jsonify, make_response from reportlab.lib.pagesizes import letter from reportlab.pdfgen import canvas from reportlab.lib import colors -from reportlab.lib.styles import getSampleStyleSheet +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont from ..db import get_db_connection, release_db_connection from ..middleware import token_required reports_blueprint = Blueprint('reports', __name__) +# ΠŸΠΎΠΏΡ‹Ρ‚ΠΊΠ° Π·Π°Ρ€Π΅Π³ΠΈΡΡ‚Ρ€ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ кирилличСский ΡˆΡ€ΠΈΡ„Ρ‚ +FONT_PATH = os.path.join(os.path.dirname(__file__), '..', 'fonts', 'DejaVuSans.ttf') +FONT_NAME = 'DejaVuSans' + +try: + if os.path.exists(FONT_PATH): + pdfmetrics.registerFont(TTFont(FONT_NAME, FONT_PATH)) + else: + # ΠŸΡ‹Ρ‚Π°Π΅ΠΌΡΡ Π½Π°ΠΉΡ‚ΠΈ систСмный ΡˆΡ€ΠΈΡ„Ρ‚ Π½Π° Windows ΠΈΠ»ΠΈ Linux + sys_fonts = [ + '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', + 'C:\\Windows\\Fonts\\arial.ttf' + ] + for f in sys_fonts: + if os.path.exists(f): + pdfmetrics.registerFont(TTFont(FONT_NAME, f)) + break +except Exception as e: + print(f"Warning: Could not register Cyrillic font: {e}") + FONT_NAME = 'Helvetica' # Fallback + @reports_blueprint.route('/reports/cases/csv', methods=['GET']) @token_required def export_cases_csv(current_user_id): + """ + Экспорт всСх кСйсов Π² CSV + --- + tags: + - Reports + security: + - APIKeyHeader: [] + responses: + 200: + description: CSV Ρ„Π°ΠΉΠ» + """ conn = get_db_connection() query = """ SELECT id, platform, violator_url, violator_article, my_article, comment, status, created_at @@ -38,6 +73,22 @@ def export_cases_csv(current_user_id): @reports_blueprint.route('/reports/case//pdf', methods=['GET']) @token_required def export_case_pdf(current_user_id, case_id): + """ + ГСнСрация PDF ΠΏΡ€Π΅Ρ‚Π΅Π½Π·ΠΈΠΈ для ΠΊΠΎΠ½ΠΊΡ€Π΅Ρ‚Π½ΠΎΠ³ΠΎ кСйса + --- + tags: + - Reports + security: + - APIKeyHeader: [] + parameters: + - name: case_id + in: path + type: integer + required: true + responses: + 200: + description: PDF Ρ„Π°ΠΉΠ» ΠΏΡ€Π΅Ρ‚Π΅Π½Π·ΠΈΠΈ + """ conn = get_db_connection() cur = conn.cursor() cur.execute(""" @@ -56,34 +107,62 @@ def export_case_pdf(current_user_id, case_id): buffer = io.BytesIO() doc = SimpleDocTemplate(buffer, pagesize=letter) styles = getSampleStyleSheet() + + # Custom style for Russian text + ru_style = ParagraphStyle( + 'Russian', + parent=styles['Normal'], + fontName=FONT_NAME, + fontSize=10, + leading=12 + ) + title_style = ParagraphStyle( + 'RussianTitle', + parent=styles['Title'], + fontName=FONT_NAME, + fontSize=18, + spaceAfter=20 + ) + elements = [] - # Title - elements.append(Paragraph(f"ΠžΡ„ΠΈΡ†ΠΈΠ°Π»ΡŒΠ½Π°Ρ прСтСнзия #{case_id}", styles['Title'])) - elements.append(Spacer(1, 12)) + # Header / Title + elements.append(Paragraph(f"ΠžΡ„ΠΈΡ†ΠΈΠ°Π»ΡŒΠ½Π°Ρ прСтСнзия #{case_id}", title_style)) + elements.append(Paragraph(f"БистСма ΠΌΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³Π° AutoMarket (OzonGuard)", ru_style)) + elements.append(Spacer(1, 20)) # Info table data = [ - ["ΠœΠ°Ρ€ΠΊΠ΅Ρ‚ΠΏΠ»Π΅ΠΉΡ", platform.upper()], - ["Π”Π°Ρ‚Π° создания", created_at.strftime('%Y-%m-%d %H:%M') if created_at else "-"], - ["Артикул Π½Π°Ρ€ΡƒΡˆΠΈΡ‚Π΅Π»Ρ", v_art], - ["Бсылка Π½Π° Ρ‚ΠΎΠ²Π°Ρ€", Paragraph(v_url, styles['Normal'])], - ["Π’Π°Ρˆ Π°Ρ€Ρ‚ΠΈΠΊΡƒΠ»", my_art or "-"], - ["Π’Π΅ΠΊΡƒΡ‰ΠΈΠΉ статус", status.upper()] + [Paragraph("ΠŸΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€", ru_style), Paragraph("Π—Π½Π°Ρ‡Π΅Π½ΠΈΠ΅", ru_style)], + [Paragraph("ΠœΠ°Ρ€ΠΊΠ΅Ρ‚ΠΏΠ»Π΅ΠΉΡ", ru_style), platform.upper()], + [Paragraph("Π”Π°Ρ‚Π° создания", ru_style), created_at.strftime('%Y-%m-%d %H:%M') if created_at else "-"], + [Paragraph("Артикул Π½Π°Ρ€ΡƒΡˆΠΈΡ‚Π΅Π»Ρ", ru_style), v_art], + [Paragraph("Бсылка Π½Π° Ρ‚ΠΎΠ²Π°Ρ€", ru_style), Paragraph(v_url, ru_style)], + [Paragraph("Π’Π°Ρˆ Π°Ρ€Ρ‚ΠΈΠΊΡƒΠ»", ru_style), my_art or "-"], + [Paragraph("Π’Π΅ΠΊΡƒΡ‰ΠΈΠΉ статус", ru_style), status.upper()] ] t = Table(data, colWidths=[150, 350]) t.setStyle(TableStyle([ - ('BACKGROUND', (0, 0), (0, -1), colors.lightgrey), - ('GRID', (0, 0), (-1, -1), 1, colors.black), - ('PADDING', (0, 0), (-1, -1), 6), + ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2c3e50')), + ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), + ('ALIGN', (0, 0), (-1, -1), 'LEFT'), + ('FONTNAME', (0, 0), (-1, -1), FONT_NAME), + ('FONTSIZE', (0, 0), (-1, -1), 10), + ('BOTTOMPADDING', (0, 0), (-1, 0), 12), + ('BACKGROUND', (0, 1), (-1, -1), colors.whitesmoke), + ('GRID', (0, 0), (-1, -1), 1, colors.grey), + ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), ])) elements.append(t) elements.append(Spacer(1, 24)) # Description - elements.append(Paragraph("ОписаниС Π½Π°Ρ€ΡƒΡˆΠ΅Π½ΠΈΡ:", styles['Heading2'])) - elements.append(Paragraph(comment or "ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ отсутствуСт", styles['Normal'])) + elements.append(Paragraph("ОписаниС Π½Π°Ρ€ΡƒΡˆΠ΅Π½ΠΈΡ:", ParagraphStyle('Sub', parent=ru_style, fontSize=12, spaceAfter=10))) + elements.append(Paragraph(comment or "ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ отсутствуСт", ru_style)) + + elements.append(Spacer(1, 40)) + elements.append(Paragraph("Π‘Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΠΎΠ²Π°Π½ΠΎ автоматичСски. Π”Π°Π½Π½Ρ‹ΠΉ Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚ являСтся ΠΎΡ„ΠΈΡ†ΠΈΠ°Π»ΡŒΠ½Ρ‹ΠΌ ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠ΅ΠΌ ΠΎ Π½Π°Ρ€ΡƒΡˆΠ΅Π½ΠΈΠΈ ΠΏΡ€Π°Π²ΠΈΠ» ΠΏΠ»Π°Ρ‚Ρ„ΠΎΡ€ΠΌΡ‹.", ParagraphStyle('Footer', parent=ru_style, fontSize=8, textColor=colors.grey))) doc.build(elements) buffer.seek(0) diff --git a/backend/routes/seo.py b/backend/routes/seo.py new file mode 100644 index 0000000..e3c72c6 --- /dev/null +++ b/backend/routes/seo.py @@ -0,0 +1,78 @@ +from flask import Blueprint, request, jsonify +from ..db import get_db_connection, release_db_connection +from ..middleware import token_required +import random +from datetime import datetime, timedelta + +seo_blueprint = Blueprint('seo', __name__) + +@seo_blueprint.route('/seo/keywords', methods=['GET']) +@token_required +def list_keywords(current_user_id): + conn = get_db_connection() + cur = conn.cursor() + cur.execute(""" + SELECT k.id, k.keyword, k.article, k.platform, + (SELECT position FROM seo_history h WHERE h.keyword_id = k.id ORDER BY checked_at DESC LIMIT 1) as last_pos + FROM seo_keywords k + WHERE k.user_id = %s + """, (current_user_id,)) + rows = cur.fetchall() + release_db_connection(conn) + + return jsonify([{ + "id": r[0], + "keyword": r[1], + "article": r[2], + "platform": r[3], + "last_position": r[4] + } for r in rows]) + +@seo_blueprint.route('/seo/keywords', methods=['POST']) +@token_required +def add_keyword(current_user_id): + data = request.json + keyword = data.get('keyword') + article = data.get('article') + platform = data.get('platform', 'ozon') + + if not keyword or not article: + return jsonify({"error": "Keyword and article are required"}), 400 + + conn = get_db_connection() + cur = conn.cursor() + cur.execute( + "INSERT INTO seo_keywords (user_id, keyword, article, platform) VALUES (%s, %s, %s, %s) RETURNING id", + (current_user_id, keyword, article, platform) + ) + kid = cur.fetchone()[0] + + # Generate some mock history immediately + for i in range(7): + date = datetime.now() - timedelta(days=i) + pos = random.randint(1, 150) + cur.execute("INSERT INTO seo_history (keyword_id, position, checked_at) VALUES (%s, %s, %s)", (kid, pos, date)) + + conn.commit() + release_db_connection(conn) + return jsonify({"id": kid}), 201 + +@seo_blueprint.route('/seo/history/', methods=['GET']) +@token_required +def get_seo_history(current_user_id, keyword_id): + conn = get_db_connection() + cur = conn.cursor() + # Check ownership + cur.execute("SELECT id FROM seo_keywords WHERE id = %s AND user_id = %s", (keyword_id, current_user_id)) + if not cur.fetchone(): + release_db_connection(conn) + return jsonify({"error": "Not found"}), 404 + + cur.execute("SELECT position, checked_at FROM seo_history WHERE keyword_id = %s ORDER BY checked_at ASC", (keyword_id,)) + rows = cur.fetchall() + release_db_connection(conn) + + return jsonify([{ + "position": r[0], + "date": r[1].strftime('%d.%m') + } for r in rows]) diff --git a/backend/utils/ai_utils.py b/backend/utils/ai_utils.py new file mode 100644 index 0000000..cf5a3aa --- /dev/null +++ b/backend/utils/ai_utils.py @@ -0,0 +1,65 @@ +import os +import google.generativeai as genai + +def generate_complaint_text(platform, violator_article, my_article=None): + """Π“Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅Ρ‚ тСкст ΠΏΡ€Π΅Ρ‚Π΅Π½Π·ΠΈΠΈ с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ Google Gemini.""" + api_key = os.getenv('GEMINI_API_KEY') + if not api_key: + return "Ошибка: API ΠΊΠ»ΡŽΡ‡ для Gemini Π½Π΅ настроСн Π² .env" + + genai.configure(api_key=api_key) + model = genai.GenerativeModel('gemini-1.5-flash') + + prompt = f""" + Π’Ρ‹ - ΠΏΡ€ΠΎΡ„Π΅ΡΡΠΈΠΎΠ½Π°Π»ΡŒΠ½Ρ‹ΠΉ ΡŽΡ€ΠΈΡΡ‚ Π² сфСрС ΠΈΠ½Ρ‚Π΅Π»Π»Π΅ΠΊΡ‚ΡƒΠ°Π»ΡŒΠ½ΠΎΠΉ собствСнности ΠΈ e-commerce. + Напиши ΠΎΡ„ΠΈΡ†ΠΈΠ°Π»ΡŒΠ½ΡƒΡŽ ΠΈ ΡƒΠ±Π΅Π΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΡƒΡŽ ΠΏΡ€Π΅Ρ‚Π΅Π½Π·ΠΈΡŽ для маркСтплСйса {platform}. + + Π‘ΡƒΡ‚ΡŒ Π½Π°Ρ€ΡƒΡˆΠ΅Π½ΠΈΡ: ΠΏΡ€ΠΎΠ΄Π°Π²Π΅Ρ† с Π°Ρ€Ρ‚ΠΈΠΊΡƒΠ»ΠΎΠΌ {violator_article} Π½Π΅Π·Π°ΠΊΠΎΠ½Π½ΠΎ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ ΠΌΠΎΠΉ ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚ ΠΈΠ»ΠΈ Π½Π°Ρ€ΡƒΡˆΠ°Π΅Ρ‚ ΠΏΡ€Π°Π²ΠΈΠ»Π° цСнообразования. + Мой ΠΎΡ€ΠΈΠ³ΠΈΠ½Π°Π»ΡŒΠ½Ρ‹ΠΉ Π°Ρ€Ρ‚ΠΈΠΊΡƒΠ» для сравнСния: {my_article if my_article else 'Π½Π΅ ΡƒΠΊΠ°Π·Π°Π½'}. + + ВрСбования: + 1. ВСкст Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ Π½Π° русском языкС. + 2. Π‘Ρ‚ΠΈΠ»ΡŒ ΠΎΡ„ΠΈΡ†ΠΈΠ°Π»ΡŒΠ½ΠΎ-Π΄Π΅Π»ΠΎΠ²ΠΎΠΉ. + 3. Упомяни ΠΎ нСобходимости Π·Π°Ρ‰ΠΈΡ‚Ρ‹ ΠΏΡ€Π°Π² правообладатСля. + 4. ВСкст Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ Π»Π°ΠΊΠΎΠ½ΠΈΡ‡Π½Ρ‹ΠΌ (Π΄ΠΎ 500 Π·Π½Π°ΠΊΠΎΠ²). + + ΠžΡ‚Π²Π΅Ρ‚ Π΄Π°ΠΉ сразу Π² Π²ΠΈΠ΄Π΅ тСкста ΠΏΡ€Π΅Ρ‚Π΅Π½Π·ΠΈΠΈ, Π±Π΅Π· Π»ΠΈΡˆΠ½ΠΈΡ… вступлСний. + """ + + try: + response = model.generate_content(prompt) + return response.text.strip() + except Exception as e: + return f"Ошибка ΠΏΡ€ΠΈ Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΠΈ тСкста ИИ: {str(e)}" + +def generate_review_reply(review_text, rating): + """Π“Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅Ρ‚ ΠΎΡ‚Π²Π΅Ρ‚ Π½Π° ΠΎΡ‚Π·Ρ‹Π² покупатСля.""" + api_key = os.getenv('GEMINI_API_KEY') + if not api_key: + return "Ошибка: API ΠΊΠ»ΡŽΡ‡ Π½Π΅ настроСн." + + genai.configure(api_key=api_key) + model = genai.GenerativeModel('gemini-1.5-flash') + + prompt = f""" + Π’Ρ‹ - ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€ ΠΏΠΎ Ρ€Π°Π±ΠΎΡ‚Π΅ с ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π°ΠΌΠΈ Π² ΠΌΠ°Π³Π°Π·ΠΈΠ½Π΅ Π½Π° маркСтплСйсС. + Ввоя Π·Π°Π΄Π°Ρ‡Π°: Π²Π΅ΠΆΠ»ΠΈΠ²ΠΎ ΠΈ ΠΏΡ€ΠΎΡ„Π΅ΡΡΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎ ΠΎΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π½Π° ΠΎΡ‚Π·Ρ‹Π² покупатСля. + + ΠžΡ†Π΅Π½ΠΊΠ° покупатСля: {rating} Π·Π²Π΅Π·Π΄(Ρ‹). + ВСкст ΠΎΡ‚Π·Ρ‹Π²Π°: "{review_text}" + + ΠŸΡ€Π°Π²ΠΈΠ»Π°: + 1. Если ΠΎΡ†Π΅Π½ΠΊΠ° 4-5 Π·Π²Π΅Π·Π΄: ΠΏΠΎΠ±Π»Π°Π³ΠΎΠ΄Π°Ρ€ΠΈ Π·Π° ΠΏΠΎΠΊΡƒΠΏΠΊΡƒ, Π²Ρ‹Ρ€Π°Π·ΠΈ Ρ€Π°Π΄ΠΎΡΡ‚ΡŒ, ΠΏΡ€Π΅Π΄Π»ΠΎΠΆΠΈ ΠΏΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Π΄Ρ€ΡƒΠ³ΠΈΠ΅ Ρ‚ΠΎΠ²Π°Ρ€Ρ‹ нашСго Π±Ρ€Π΅Π½Π΄Π°. + 2. Если ΠΎΡ†Π΅Π½ΠΊΠ° 1-3 Π·Π²Π΅Π·Π΄Ρ‹: извинись Π·Π° нСудобства, ΡƒΡ‚ΠΎΡ‡Π½ΠΈ, Ρ‡Ρ‚ΠΎ ΠΌΡ‹ Ρ€Π°Π±ΠΎΡ‚Π°Π΅ΠΌ Π½Π°Π΄ качСством, ΠΈ ΠΏΡ€Π΅Π΄Π»ΠΎΠΆΠΈ ΠΎΠ±Ρ€Π°Ρ‚ΠΈΡ‚ΡŒΡΡ Π² ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΡƒ, Ссли это умСстно. + 3. Π‘Ρ‚ΠΈΠ»ΡŒ: Π΄Ρ€ΡƒΠΆΠ΅Π»ΡŽΠ±Π½Ρ‹ΠΉ, Π½ΠΎ Π΄Π΅Π»ΠΎΠ²ΠΎΠΉ. НС ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉ слишком ΠΌΠ½ΠΎΠ³ΠΎ Π²ΠΎΡΠΊΠ»ΠΈΡ†Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… Π·Π½Π°ΠΊΠΎΠ². + 4. Π”Π»ΠΈΠ½Π°: 2-4 прСдлоТСния. + 5. НС ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉ ΡˆΠ°Π±Π»ΠΎΠ½Ρ‹ "Π£Π²Π°ΠΆΠ°Π΅ΠΌΡ‹ΠΉ ΠΏΠΎΠΊΡƒΠΏΠ°Ρ‚Π΅Π»ΡŒ", Π½Π°Ρ‡Π½ΠΈ Π±ΠΎΠ»Π΅Π΅ СстСствСнно. + + ΠžΡ‚Π²Π΅Ρ‚ Π΄Π°ΠΉ сразу Π² Π²ΠΈΠ΄Π΅ Π³ΠΎΡ‚ΠΎΠ²ΠΎΠ³ΠΎ тСкста. + """ + + try: + response = model.generate_content(prompt) + return response.text.strip() + except Exception as e: + return f"Ошибка ИИ: {str(e)}" diff --git a/backend/utils/audit_utils.py b/backend/utils/audit_utils.py new file mode 100644 index 0000000..966ea76 --- /dev/null +++ b/backend/utils/audit_utils.py @@ -0,0 +1,17 @@ +import json +from ..db import get_db_connection, release_db_connection + +def log_action(user_id, action, details=None): + """ЗаписываСт дСйствиС ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π² Π»ΠΎΠ³ Π°ΡƒΠ΄ΠΈΡ‚Π°.""" + conn = get_db_connection() + try: + cur = conn.cursor() + cur.execute( + "INSERT INTO audit_log (user_id, action, details) VALUES (%s, %s, %s)", + (user_id, action, json.dumps(details) if details else None) + ) + conn.commit() + except Exception as e: + print(f"Audit Log Error: {e}") + finally: + release_db_connection(conn) diff --git a/backend/utils/image_utils.py b/backend/utils/image_utils.py new file mode 100644 index 0000000..6d49239 --- /dev/null +++ b/backend/utils/image_utils.py @@ -0,0 +1,20 @@ +from PIL import Image +import imagehash +import os + +def calculate_image_hash(image_path): + """ВычисляСт ΠΏΠ΅Ρ€Ρ†Π΅ΠΏΡ‚ΠΈΠ²Π½Ρ‹ΠΉ Ρ…Π΅Ρˆ изобраТСния.""" + try: + img = Image.open(image_path) + hash_val = imagehash.phash(img) + return str(hash_val) + except Exception as e: + print(f"Hashing Error: {e}") + return None + +def compare_hashes(hash1, hash2, threshold=5): + """Π‘Ρ€Π°Π²Π½ΠΈΠ²Π°Π΅Ρ‚ Π΄Π²Π° Ρ…Π΅ΡˆΠ°. Если Ρ€Π°Π·Π½ΠΈΡ†Π° мСньшС ΠΏΠΎΡ€ΠΎΠ³Π°, изобраТСния ΡΡ‡ΠΈΡ‚Π°ΡŽΡ‚ΡΡ ΠΏΠΎΡ…ΠΎΠΆΠΈΠΌΠΈ.""" + h1 = imagehash.hex_to_hash(hash1) + h2 = imagehash.hex_to_hash(hash2) + diff = h1 - h2 + return diff <= threshold diff --git a/backend/utils/telegram_utils.py b/backend/utils/telegram_utils.py new file mode 100644 index 0000000..04652d2 --- /dev/null +++ b/backend/utils/telegram_utils.py @@ -0,0 +1,24 @@ +import requests +import os + +def send_telegram_notification(message): + token = os.getenv('TELEGRAM_BOT_TOKEN') + chat_id = os.getenv('TELEGRAM_CHAT_ID') + + if not token or not chat_id: + print("Telegram notification skipped: Token or Chat ID not configured.") + return False + + url = f"https://api.telegram.org/bot{token}/sendMessage" + payload = { + "chat_id": chat_id, + "text": message, + "parse_mode": "HTML" + } + + try: + response = requests.post(url, json=payload, timeout=5) + return response.status_code == 200 + except Exception as e: + print(f"Error sending Telegram message: {e}") + return False diff --git a/db/init.sql b/db/init.sql index f74ed51..0e58239 100644 --- a/db/init.sql +++ b/db/init.sql @@ -39,3 +39,36 @@ CREATE TABLE IF NOT EXISTS files ( file_path TEXT NOT NULL, uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); + +CREATE TABLE IF NOT EXISTS audit_log ( + id SERIAL PRIMARY KEY, + user_id INT REFERENCES users(id) ON DELETE CASCADE, + action VARCHAR(100) NOT NULL, + details JSONB, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS seo_keywords ( + id SERIAL PRIMARY KEY, + user_id INT REFERENCES users(id) ON DELETE CASCADE, + keyword VARCHAR(255) NOT NULL, + article VARCHAR(100) NOT NULL, + platform VARCHAR(50) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS seo_history ( + id SERIAL PRIMARY KEY, + keyword_id INT REFERENCES seo_keywords(id) ON DELETE CASCADE, + position INT, + checked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS reference_images ( + id SERIAL PRIMARY KEY, + user_id INT REFERENCES users(id) ON DELETE CASCADE, + file_path TEXT NOT NULL, + image_hash VARCHAR(64), + article VARCHAR(100), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e39b48f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +# docker-compose.yml +version: '3.8' + +services: + backend: + build: + context: . + dockerfile: backend/Dockerfile + ports: + - "5000:5000" + environment: + # Для Windows/Mac ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ host.docker.internal для доступа ΠΊ локальной Π‘Π” + - DATABASE_URL=postgresql://user:password@host.docker.internal:5432/automarket + - JWT_SECRET=your_super_secret_key + volumes: + - ./backend/uploads:/app/backend/uploads + extra_hosts: + - "host.docker.internal:host-gateway" + + frontend: + image: nginx:alpine + ports: + - "80:80" + volumes: + - ./frontend:/usr/share/nginx/html + depends_on: + - backend diff --git a/frontend/css/styles.css b/frontend/css/styles.css index 6f5309c..50add40 100644 --- a/frontend/css/styles.css +++ b/frontend/css/styles.css @@ -1,22 +1,31 @@ /* frontend/css/styles.css */ -/* Premium dark‑mode + glassmorphism design */ +/* Ultimate Premium Dark Mode + Glassmorphism */ :root { - --font-family: 'Inter', sans-serif; - --color-bg: hsl(220, 10%, 10%); - --color-surface: hsla(220, 10%, 15%, 0.8); - --color-primary: hsl(210, 80%, 55%); - --color-primary-hover: hsl(210, 80%, 45%); - --color-success: hsl(140, 60%, 45%); - --color-error: hsl(0, 70%, 50%); - --color-text: hsl(0, 0%, 90%); - --radius: 0.75rem; - --shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + --font-family: 'Inter', system-ui, -apple-system, sans-serif; + + /* Palette */ + --color-bg: #0b0e14; + --color-surface: rgba(23, 28, 38, 0.7); + --color-surface-hover: rgba(35, 42, 56, 0.85); + --color-primary: #3b82f6; /* Modern Blue */ + --color-primary-glow: rgba(59, 130, 246, 0.5); + --color-success: #10b981; /* Emerald */ + --color-success-glow: rgba(16, 185, 129, 0.4); + --color-warning: #f59e0b; /* Amber */ + --color-error: #ef4444; /* Rose */ + --color-text: #f3f4f6; + --color-text-dim: #9ca3af; + + /* Utils */ + --radius-lg: 1.25rem; + --radius-md: 0.75rem; + --shadow-sm: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.1); + --glass-border: 1px solid rgba(255, 255, 255, 0.08); } -*, -*::before, -*::after { +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; @@ -24,248 +33,307 @@ body { font-family: var(--font-family); - background: var(--color-bg); + background-color: var(--color-bg); + background-image: + radial-gradient(circle at 10% 20%, rgba(59, 130, 246, 0.05) 0%, transparent 40%), + radial-gradient(circle at 90% 80%, rgba(16, 185, 129, 0.05) 0%, transparent 40%); color: var(--color-text); - line-height: 1.6; + line-height: 1.5; min-height: 100vh; - display: flex; - flex-direction: column; + -webkit-font-smoothing: antialiased; } +/* Custom Scrollbar */ +::-webkit-scrollbar { width: 8px; } +::-webkit-scrollbar-track { background: var(--color-bg); } +::-webkit-scrollbar-thumb { + background: rgba(255,255,255,0.1); + border-radius: 10px; +} +::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.2); } + +/* Header */ .header { - text-align: center; - padding: 2rem 1rem; - background: linear-gradient(135deg, hsla(210, 70%, 55%, 0.6), hsla(210, 70%, 35%, 0.6)); - backdrop-filter: blur(12px); - box-shadow: var(--shadow); + padding: 2.5rem 1rem; + background: rgba(11, 14, 20, 0.8); + backdrop-filter: blur(20px); + border-bottom: var(--glass-border); + position: sticky; + top: 0; + z-index: 100; } .header h1 { - font-size: 2.2rem; - margin-bottom: 0.5rem; - color: var(--color-primary); + font-size: 2.5rem; + font-weight: 800; + background: linear-gradient(to right, #60a5fa, #34d399); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + letter-spacing: -0.025em; } -.container { - flex: 1; - padding: 1.5rem; - max-width: 1200px; - margin: 0 auto; +.header p { + color: var(--color-text-dim); + font-size: 0.95rem; } -.section { +/* Auth Section */ +.auth-box { + max-width: 420px; + margin: 6rem auto; + padding: 2.5rem; + background: var(--color-surface); + backdrop-filter: blur(16px); + border-radius: var(--radius-lg); + border: var(--glass-border); + box-shadow: var(--shadow-xl); +} + +.auth-box h2 { + font-size: 1.75rem; margin-bottom: 2rem; + text-align: center; } -h2 { - margin-bottom: 1rem; - font-weight: 600; - color: var(--color-primary); -} - -.controls { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - margin-bottom: 1rem; -} - -.controls input, -.controls select { - padding: 0.5rem 0.75rem; - border: 1px solid hsla(0, 0%, 100%, 0.1); - border-radius: var(--radius); - background: var(--color-surface); - color: var(--color-text); -} - -.btn-primary { - background: var(--color-primary); - border: none; +.auth-box input { + width: 100%; + padding: 0.875rem 1.25rem; + margin-top: 0.75rem; + background: rgba(0, 0, 0, 0.3); + border: var(--glass-border); + border-radius: var(--radius-md); color: #fff; - padding: 0.5rem 1rem; - border-radius: var(--radius); - cursor: pointer; - transition: background 0.2s; + transition: all 0.2s; } -.btn-primary:hover { - background: var(--color-primary-hover); -} - -.btn-secondary { - background: hsla(0, 0%, 100%, 0.1); - border: none; - color: var(--color-text); - padding: 0.5rem 1rem; - border-radius: var(--radius); - cursor: pointer; - transition: background 0.2s; -} - -.btn-secondary:hover { - background: hsla(0, 0%, 100%, 0.2); -} - -.table { - width: 100%; - border-collapse: collapse; - backdrop-filter: blur(6px); - background: var(--color-surface); - border-radius: var(--radius); - overflow: hidden; - box-shadow: var(--shadow); -} - -.table th, -.table td { - padding: 0.75rem 1rem; - text-align: left; - border-bottom: 1px solid hsla(0, 0%, 100%, 0.05); -} - -.table th { - background: hsla(0, 0%, 100%, 0.05); - color: var(--color-primary); -} - -.hidden { display: none; } - -label { - display: block; - margin-bottom: 0.75rem; - color: var(--color-text); -} - -label input, -label textarea { - width: 100%; - padding: 0.5rem; - margin-top: 0.25rem; - border: 1px solid hsla(0,0%,100%,0.1); - border-radius: var(--radius); - background: var(--color-surface); - color: var(--color-text); -} - -.preview { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - margin-top: 0.5rem; -} - -.preview img { - max-width: 120px; - max-height: 120px; - object-fit: cover; - border-radius: var(--radius); - box-shadow: var(--shadow); +.auth-box input:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px var(--color-primary-glow); } +/* Tabs */ .tabs-container { display: flex; justify-content: center; - gap: 1rem; - padding: 1rem; - background: var(--color-surface); - border-bottom: 1px solid hsla(0,0%,100%,0.1); + gap: 0.5rem; + padding: 0.75rem; + background: rgba(255, 255, 255, 0.03); + margin-top: 1rem; + border-radius: var(--radius-md); + max-width: 800px; + margin-left: auto; + margin-right: auto; } .tab-btn { background: transparent; - color: var(--color-text); + color: var(--color-text-dim); border: none; - padding: 0.5rem 1rem; - font-size: 1.1rem; + padding: 0.625rem 1.25rem; + border-radius: var(--radius-md); cursor: pointer; - border-bottom: 2px solid transparent; + font-weight: 500; transition: all 0.2s; } .tab-btn:hover { - color: var(--color-primary); + background: rgba(255, 255, 255, 0.05); + color: #fff; } .tab-btn.active { - color: var(--color-primary); - border-bottom: 2px solid var(--color-primary); + background: var(--color-primary); + color: #fff; + box-shadow: 0 4px 12px var(--color-primary-glow); } +/* Main Content */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 2.5rem 1.5rem; +} + +.section { + animation: fadeIn 0.4s ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +/* Cards & Grid */ .platforms-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1.5rem; } .platform-card { background: var(--color-surface); - padding: 1.5rem; - border-radius: var(--radius); - border: 1px solid hsla(0,0%,100%,0.1); - transition: transform 0.2s; + padding: 1.75rem; + border-radius: var(--radius-lg); + border: var(--glass-border); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } .platform-card:hover { - transform: translateY(-2px); + transform: translateY(-4px); + background: var(--color-surface-hover); + border-color: rgba(255,255,255,0.15); + box-shadow: var(--shadow-xl); } .platform-card h3 { - color: var(--color-primary); - margin-bottom: 0.5rem; + font-size: 1.25rem; + margin-bottom: 0.75rem; } -.platform-card .status { - font-size: 0.9rem; - margin-bottom: 1rem; +/* Buttons */ +.btn-primary { + background: var(--color-primary); + color: #fff; + font-weight: 600; + padding: 0.75rem 1.5rem; + border-radius: var(--radius-md); + border: none; + cursor: pointer; + transition: all 0.2s; + display: inline-flex; + align-items: center; + justify-content: center; } -.active-platform { - border-color: var(--color-success); +.btn-primary:hover { + filter: brightness(1.1); + transform: translateY(-1px); + box-shadow: 0 4px 15px var(--color-primary-glow); } -.active-platform h3 { - color: var(--color-success); +.btn-secondary { + background: rgba(255, 255, 255, 0.05); + color: var(--color-text); + font-weight: 500; + padding: 0.75rem 1.5rem; + border-radius: var(--radius-md); + border: var(--glass-border); + cursor: pointer; + transition: all 0.2s; } -.platform-form { +.btn-secondary:hover { + background: rgba(255, 255, 255, 0.1); +} + +/* Table */ +.table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + background: var(--color-surface); + border-radius: var(--radius-lg); + overflow: hidden; + border: var(--glass-border); +} + +.table th { + background: rgba(255, 255, 255, 0.03); + padding: 1rem 1.5rem; + text-transform: uppercase; + font-size: 0.75rem; + letter-spacing: 0.05em; + color: var(--color-text-dim); + text-align: left; +} + +.table td { + padding: 1.25rem 1.5rem; + border-top: var(--glass-border); + font-size: 0.95rem; +} + +.table tr:hover td { + background: rgba(255, 255, 255, 0.02); +} + +/* Status Badges */ +.status-pill { + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.8rem; + font-weight: 600; + text-transform: uppercase; +} +.status-new { background: rgba(59, 130, 246, 0.15); color: #60a5fa; } +.status-closed { background: rgba(16, 185, 129, 0.15); color: #34d399; } + +/* Icons */ +.icon-sm { + width: 18px; + height: 18px; + vertical-align: middle; + margin-right: 0.25rem; + stroke-width: 2.5px; +} + +.icon-sm.spin { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.hidden { display: none !important; } + +/* Form Controls */ +input, select, textarea { + font-family: inherit; + transition: border-color 0.2s; +} + +/* Chart Container */ +canvas { + margin-top: 1rem; + filter: drop-shadow(0 0 10px rgba(59, 130, 246, 0.1)); +} +/* Toast Notifications */ +.toast-container { + position: fixed; + bottom: 2rem; + right: 2rem; + z-index: 1000; display: flex; flex-direction: column; - gap: 0.5rem; + gap: 0.75rem; } -.platform-form input { - padding: 0.5rem; - border: 1px solid hsla(0,0%,100%,0.1); - border-radius: var(--radius); - background: rgba(0,0,0,0.2); - color: #fff; -} - -.auth-box { - max-width: 400px; - margin: 4rem auto; - padding: 2rem; +.toast { background: var(--color-surface); - border-radius: var(--radius); - border: 1px solid hsla(0,0%,100%,0.1); - box-shadow: var(--shadow); -} - -.auth-box input { - width: 100%; - padding: 0.75rem; - margin-top: 0.5rem; - border: 1px solid hsla(0,0%,100%,0.1); - border-radius: var(--radius); - background: rgba(0,0,0,0.2); + backdrop-filter: blur(20px); + border: var(--glass-border); + padding: 1rem 1.5rem; + border-radius: var(--radius-md); + box-shadow: var(--shadow-xl); color: #fff; + min-width: 300px; + display: flex; + align-items: center; + gap: 0.75rem; + animation: slideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1); + border-left: 4px solid var(--color-primary); } -.auth-box a { - color: var(--color-primary); - text-decoration: none; -} -.auth-box a:hover { - text-decoration: underline; +@keyframes slideIn { + from { opacity: 0; transform: translateX(100%); } + to { opacity: 1; transform: translateX(0); } } + +.toast.success { border-left-color: var(--color-success); } +.toast.error { border-left-color: var(--color-error); } +.toast.info { border-left-color: var(--color-primary); } + +.toast-message { font-size: 0.9rem; font-weight: 500; } diff --git a/frontend/index.html b/frontend/index.html index 408349f..c742ae3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,13 +5,16 @@ AutoMarket – Π£Π½ΠΈΠ²Π΅Ρ€ΡΠ°Π»ΡŒΠ½Π°Ρ панСль управлСния - + + + +
-

AutoMarket

+

AutoMarket

Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ ΠΆΠ°Π»ΠΎΠ±Π°ΠΌΠΈ ΠΈ интСграциями для 10 маркСтплСйсов