🚀 MVP: FastAPI + React форма с SMS верификацией
✅ Инфраструктура: PostgreSQL, Redis, RabbitMQ, S3 ✅ Backend: SMS сервис + API endpoints ✅ Frontend: React форма (3 шага) + SMS верификация
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -54,3 +54,4 @@ htmlcov/
|
|||||||
*.tmp
|
*.tmp
|
||||||
*.bak
|
*.bak
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
41
LINKS.md
Normal file
41
LINKS.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# 🔗 ССЫЛКИ ДЛЯ ДОСТУПА
|
||||||
|
|
||||||
|
## После запуска открывай эти адреса:
|
||||||
|
|
||||||
|
### Frontend (React приложение):
|
||||||
|
http://147.45.146.17:5173/
|
||||||
|
|
||||||
|
### Backend API:
|
||||||
|
http://147.45.146.17:8100/
|
||||||
|
|
||||||
|
### API Документация (Swagger):
|
||||||
|
http://147.45.146.17:8100/docs
|
||||||
|
|
||||||
|
### Health Check:
|
||||||
|
http://147.45.146.17:8100/health
|
||||||
|
|
||||||
|
### Test Endpoint:
|
||||||
|
http://147.45.146.17:8100/api/v1/test
|
||||||
|
|
||||||
|
### Gitea (Git репозиторий):
|
||||||
|
http://147.45.146.17:3002/negodiy/erv-platform
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Команды для запуска:
|
||||||
|
|
||||||
|
### Терминал 1 - Backend:
|
||||||
|
```bash
|
||||||
|
cd /var/www/fastuser/data/www/crm.clientright.ru/erv_platform/backend
|
||||||
|
source venv/bin/activate
|
||||||
|
uvicorn app.main:app --reload --host 0.0.0.0 --port 8100
|
||||||
|
```
|
||||||
|
|
||||||
|
### Терминал 2 - Frontend:
|
||||||
|
```bash
|
||||||
|
cd /var/www/fastuser/data/www/crm.clientright.ru/erv_platform/frontend
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -584,3 +584,4 @@ server {
|
|||||||
Сейчас создам всю базовую структуру и запущу оба приложения (FastAPI + React).
|
Сейчас создам всю базовую структуру и запущу оба приложения (FastAPI + React).
|
||||||
|
|
||||||
**Начинаю прямо сейчас!** 🚀
|
**Начинаю прямо сейчас!** 🚀
|
||||||
|
|
||||||
|
|||||||
@@ -181,3 +181,4 @@ git push -u origin main
|
|||||||
|
|
||||||
**Удачи!** 🚀
|
**Удачи!** 🚀
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -170,3 +170,4 @@ git push origin main
|
|||||||
**Автор**: AI Assistant + Фёдор
|
**Автор**: AI Assistant + Фёдор
|
||||||
**Дата**: 24.10.2025
|
**Дата**: 24.10.2025
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
173
START_HERE.md
Normal file
173
START_HERE.md
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# ⚡ ЗАПУСК MVP - ИНСТРУКЦИЯ ДЛЯ ФЁДОРА
|
||||||
|
|
||||||
|
## 🎯 Что сделано:
|
||||||
|
|
||||||
|
✅ FastAPI backend (Python)
|
||||||
|
✅ React frontend (TypeScript)
|
||||||
|
✅ Git репозиторий (Gitea)
|
||||||
|
✅ Конфигурация (.env)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 КАК ЗАПУСТИТЬ (2 команды):
|
||||||
|
|
||||||
|
### **Команда 1: Backend (FastAPI)**
|
||||||
|
|
||||||
|
Открой **ТЕРМИНАЛ 1** и выполни:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/fastuser/data/www/crm.clientright.ru/erv_platform/backend
|
||||||
|
source venv/bin/activate
|
||||||
|
uvicorn app.main:app --reload --host 0.0.0.0 --port 8100
|
||||||
|
```
|
||||||
|
|
||||||
|
Увидишь:
|
||||||
|
```
|
||||||
|
🚀 ERV Insurance Platform запускается...
|
||||||
|
📍 Backend URL: http://localhost:8100
|
||||||
|
📍 API Docs: http://localhost:8100/docs
|
||||||
|
INFO: Uvicorn running on http://0.0.0.0:8100
|
||||||
|
```
|
||||||
|
|
||||||
|
**НЕ ЗАКРЫВАЙ этот терминал!** Сервер должен работать.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Команда 2: Frontend (React)**
|
||||||
|
|
||||||
|
Открой **ТЕРМИНАЛ 2** (новый!) и выполни:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/fastuser/data/www/crm.clientright.ru/erv_platform/frontend
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Увидишь:
|
||||||
|
```
|
||||||
|
VITE v5.x.x ready in XXX ms
|
||||||
|
➜ Local: http://localhost:5173/
|
||||||
|
➜ Network: http://147.45.146.17:5173/
|
||||||
|
```
|
||||||
|
|
||||||
|
**НЕ ЗАКРЫВАЙ этот терминал!** Сервер должен работать.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 ОТКРОЙ В БРАУЗЕРЕ:
|
||||||
|
|
||||||
|
### **1. Frontend (главная страница):**
|
||||||
|
```
|
||||||
|
http://147.45.146.17:5173/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Увидишь:**
|
||||||
|
- ✅ Информацию о платформе
|
||||||
|
- ✅ Статус всех сервисов (Redis, PostgreSQL, OCR)
|
||||||
|
- ✅ Список возможностей
|
||||||
|
- ✅ Технологический стек
|
||||||
|
|
||||||
|
### **2. API Документация (Swagger UI):**
|
||||||
|
```
|
||||||
|
http://147.45.146.17:8100/docs
|
||||||
|
```
|
||||||
|
|
||||||
|
**Увидишь:**
|
||||||
|
- ✅ Список всех API endpoints
|
||||||
|
- ✅ Можно тестировать прямо в браузере!
|
||||||
|
- ✅ Автоматическая документация
|
||||||
|
|
||||||
|
### **3. Health Check:**
|
||||||
|
```
|
||||||
|
http://147.45.146.17:8100/health
|
||||||
|
```
|
||||||
|
|
||||||
|
**Увидишь:**
|
||||||
|
- ✅ Статус каждого сервиса (Redis, PostgreSQL, OCR)
|
||||||
|
- ✅ OK или ERROR для каждого
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Если что-то не работает:
|
||||||
|
|
||||||
|
### **Backend не запускается?**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверь порт 8100 свободен
|
||||||
|
netstat -tuln | grep 8100
|
||||||
|
|
||||||
|
# Если занят - используй другой порт:
|
||||||
|
uvicorn app.main:app --reload --host 0.0.0.0 --port 8200
|
||||||
|
# Тогда меняй везде 8100 на 8200
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Frontend не запускается?**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверь Node.js версию
|
||||||
|
node --version
|
||||||
|
|
||||||
|
# Если < 18, обнови:
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||||
|
sudo apt-get install -y nodejs
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Нет соединения между Frontend и Backend?**
|
||||||
|
|
||||||
|
Проверь в `frontend/vite.config.ts`:
|
||||||
|
```typescript
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8100', ← Должен совпадать с портом backend
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Проверка что всё работает:
|
||||||
|
|
||||||
|
После запуска **ОБОИХ** серверов, проверь:
|
||||||
|
|
||||||
|
1. ✅ `http://147.45.146.17:8100/` → должен вернуть JSON
|
||||||
|
2. ✅ `http://147.45.146.17:8100/health` → статус сервисов
|
||||||
|
3. ✅ `http://147.45.146.17:5173/` → красивая страница с информацией
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Что дальше:
|
||||||
|
|
||||||
|
После того как убедишься что **МВП работает**:
|
||||||
|
|
||||||
|
1. Скажешь мне: "Работает!" или "Не работает, вот ошибка..."
|
||||||
|
2. Если работает → я продолжу создавать полную функциональность:
|
||||||
|
- API для OCR документов
|
||||||
|
- API для проверки рейсов
|
||||||
|
- React компоненты формы
|
||||||
|
- Автозаполнение
|
||||||
|
- WebSocket real-time
|
||||||
|
- И т.д.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎁 Бонус - полезные команды:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Остановить Backend
|
||||||
|
# Ctrl+C в терминале где запущен uvicorn
|
||||||
|
|
||||||
|
# Остановить Frontend
|
||||||
|
# Ctrl+C в терминале где запущен npm run dev
|
||||||
|
|
||||||
|
# Посмотреть логи Backend
|
||||||
|
tail -f /var/www/fastuser/data/www/crm.clientright.ru/erv_platform/logs/backend.log
|
||||||
|
|
||||||
|
# Gitea репозиторий
|
||||||
|
http://147.45.146.17:3002/negodiy/erv-platform
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**ЗАПУСКАЙ И ПИШИ ЧТО ПОЛУЧИЛОСЬ!** 🚀
|
||||||
|
|
||||||
|
|
||||||
21
backend/Dockerfile
Normal file
21
backend/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Python FastAPI Backend Dockerfile
|
||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
# Устанавливаем рабочую директорию
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копируем requirements.txt
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Устанавливаем зависимости
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Копируем исходный код
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Открываем порт
|
||||||
|
EXPOSE 8100
|
||||||
|
|
||||||
|
# Запускаем приложение
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8100"]
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
"""
|
||||||
|
API Routes
|
||||||
|
"""
|
||||||
|
|
||||||
|
|||||||
51
backend/app/api/claims.py
Normal file
51
backend/app/api/claims.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
"""
|
||||||
|
Claims API Routes - Обработка заявок
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from .models import ClaimCreateRequest, ClaimResponse
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/v1/claims", tags=["Claims"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/create", response_model=ClaimResponse)
|
||||||
|
async def create_claim(claim: ClaimCreateRequest):
|
||||||
|
"""
|
||||||
|
Создать новую заявку
|
||||||
|
|
||||||
|
Принимает данные формы и создает заявку в системе
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Генерируем ID и номер заявки
|
||||||
|
claim_id = str(uuid.uuid4())
|
||||||
|
claim_number = f"ERV-{datetime.now().strftime('%Y%m%d')}-{claim_id[:8].upper()}"
|
||||||
|
|
||||||
|
# TODO: Сохранить в PostgreSQL
|
||||||
|
# TODO: Отправить в очередь RabbitMQ для обработки
|
||||||
|
# TODO: Интеграция с CRM
|
||||||
|
|
||||||
|
return ClaimResponse(
|
||||||
|
success=True,
|
||||||
|
claim_id=claim_id,
|
||||||
|
claim_number=claim_number,
|
||||||
|
message=f"Заявка {claim_number} успешно создана"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"Ошибка при создании заявки: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{claim_id}")
|
||||||
|
async def get_claim(claim_id: str):
|
||||||
|
"""Получить информацию о заявке по ID"""
|
||||||
|
# TODO: Получить из БД
|
||||||
|
return {
|
||||||
|
"claim_id": claim_id,
|
||||||
|
"status": "processing",
|
||||||
|
"message": "Заявка в обработке"
|
||||||
|
}
|
||||||
|
|
||||||
64
backend/app/api/models.py
Normal file
64
backend/app/api/models.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
"""
|
||||||
|
Pydantic модели для API
|
||||||
|
"""
|
||||||
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
from typing import Optional, List
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
|
||||||
|
class SMSSendRequest(BaseModel):
|
||||||
|
"""Запрос на отправку SMS кода"""
|
||||||
|
phone: str = Field(..., description="Номер телефона в формате +79001234567")
|
||||||
|
|
||||||
|
@field_validator('phone')
|
||||||
|
@classmethod
|
||||||
|
def validate_phone(cls, v: str) -> str:
|
||||||
|
# Убираем все кроме цифр и +
|
||||||
|
clean = ''.join(c for c in v if c.isdigit() or c == '+')
|
||||||
|
if not clean.startswith('+'):
|
||||||
|
clean = '+' + clean
|
||||||
|
if len(clean) != 12: # +7 + 10 цифр
|
||||||
|
raise ValueError('Неверный формат телефона')
|
||||||
|
return clean
|
||||||
|
|
||||||
|
|
||||||
|
class SMSVerifyRequest(BaseModel):
|
||||||
|
"""Запрос на проверку SMS кода"""
|
||||||
|
phone: str = Field(..., description="Номер телефона")
|
||||||
|
code: str = Field(..., min_length=6, max_length=6, description="6-значный код")
|
||||||
|
|
||||||
|
|
||||||
|
class ClaimCreateRequest(BaseModel):
|
||||||
|
"""Запрос на создание заявки"""
|
||||||
|
# Шаг 1: Основная информация
|
||||||
|
phone: str
|
||||||
|
email: Optional[str] = None
|
||||||
|
inn: Optional[str] = None
|
||||||
|
policy_number: str
|
||||||
|
policy_series: Optional[str] = None
|
||||||
|
|
||||||
|
# Шаг 2: Данные о происшествии
|
||||||
|
incident_date: Optional[str] = None
|
||||||
|
incident_description: Optional[str] = None
|
||||||
|
transport_type: Optional[str] = None # "air", "train", "bus", etc.
|
||||||
|
|
||||||
|
# Шаг 3: Данные для выплаты
|
||||||
|
payment_method: str = "sbp" # "sbp", "card", "bank_transfer"
|
||||||
|
bank_name: Optional[str] = None
|
||||||
|
card_number: Optional[str] = None
|
||||||
|
account_number: Optional[str] = None
|
||||||
|
|
||||||
|
# Файлы (UUID после загрузки)
|
||||||
|
uploaded_files: Optional[List[str]] = []
|
||||||
|
|
||||||
|
# Метаданные
|
||||||
|
source: str = "web_form"
|
||||||
|
|
||||||
|
|
||||||
|
class ClaimResponse(BaseModel):
|
||||||
|
"""Ответ после создания заявки"""
|
||||||
|
success: bool
|
||||||
|
claim_id: Optional[str] = None
|
||||||
|
claim_number: Optional[str] = None
|
||||||
|
message: str
|
||||||
|
|
||||||
53
backend/app/api/sms.py
Normal file
53
backend/app/api/sms.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
"""
|
||||||
|
SMS API Routes
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from ..services.sms_service import sms_service
|
||||||
|
from .models import SMSSendRequest, SMSVerifyRequest
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/v1/sms", tags=["SMS"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/send")
|
||||||
|
async def send_sms_code(request: SMSSendRequest):
|
||||||
|
"""
|
||||||
|
Отправить SMS код верификации
|
||||||
|
|
||||||
|
- **phone**: Номер телефона в формате +79001234567
|
||||||
|
"""
|
||||||
|
code = await sms_service.send_verification_code(request.phone)
|
||||||
|
|
||||||
|
if code:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Код отправлен на указанный номер",
|
||||||
|
"debug_code": code if sms_service.enabled else None # Показываем код только в dev
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=429,
|
||||||
|
detail="Слишком много запросов. Попробуйте через минуту."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/verify")
|
||||||
|
async def verify_sms_code(request: SMSVerifyRequest):
|
||||||
|
"""
|
||||||
|
Проверить SMS код
|
||||||
|
|
||||||
|
- **phone**: Номер телефона
|
||||||
|
- **code**: 6-значный код из SMS
|
||||||
|
"""
|
||||||
|
is_valid = await sms_service.verify_code(request.phone, request.code)
|
||||||
|
|
||||||
|
if is_valid:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Код подтвержден"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Неверный код или код истек"
|
||||||
|
)
|
||||||
|
|
||||||
@@ -3,10 +3,13 @@
|
|||||||
"""
|
"""
|
||||||
from pydantic_settings import BaseSettings
|
from pydantic_settings import BaseSettings
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
# App
|
# ============================================
|
||||||
|
# APPLICATION
|
||||||
|
# ============================================
|
||||||
app_name: str = "ERV Insurance Platform"
|
app_name: str = "ERV Insurance Platform"
|
||||||
app_env: str = "development"
|
app_env: str = "development"
|
||||||
debug: bool = True
|
debug: bool = True
|
||||||
@@ -16,47 +19,144 @@ class Settings(BaseSettings):
|
|||||||
backend_url: str = "http://localhost:8100"
|
backend_url: str = "http://localhost:8100"
|
||||||
frontend_url: str = "http://localhost:5173"
|
frontend_url: str = "http://localhost:5173"
|
||||||
|
|
||||||
# PostgreSQL
|
# ============================================
|
||||||
|
# DATABASE (PostgreSQL)
|
||||||
|
# ============================================
|
||||||
postgres_host: str = "147.45.189.234"
|
postgres_host: str = "147.45.189.234"
|
||||||
postgres_port: int = 5432
|
postgres_port: int = 5432
|
||||||
postgres_db: str = "default_db"
|
postgres_db: str = "default_db"
|
||||||
postgres_user: str = "gen_user"
|
postgres_user: str = "gen_user"
|
||||||
postgres_password: str = "2~~9_^kVsU?2\\S"
|
postgres_password: str = "2~~9_^kVsU?2\\S"
|
||||||
|
|
||||||
# Redis
|
@property
|
||||||
|
def database_url(self) -> str:
|
||||||
|
"""Формирует URL для подключения к PostgreSQL"""
|
||||||
|
return f"postgresql+asyncpg://{self.postgres_user}:{self.postgres_password}@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}"
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# REDIS
|
||||||
|
# ============================================
|
||||||
redis_host: str = "localhost"
|
redis_host: str = "localhost"
|
||||||
redis_port: int = 6379
|
redis_port: int = 6379
|
||||||
redis_password: str = "CRM_Redis_Pass_2025_Secure!"
|
redis_password: str = "CRM_Redis_Pass_2025_Secure!"
|
||||||
redis_db: int = 0
|
redis_db: int = 0
|
||||||
redis_prefix: str = "erv:"
|
redis_prefix: str = "erv:"
|
||||||
|
|
||||||
# RabbitMQ
|
@property
|
||||||
|
def redis_url(self) -> str:
|
||||||
|
"""Формирует URL для подключения к Redis"""
|
||||||
|
if self.redis_password:
|
||||||
|
return f"redis://:{self.redis_password}@{self.redis_host}:{self.redis_port}/{self.redis_db}"
|
||||||
|
return f"redis://{self.redis_host}:{self.redis_port}/{self.redis_db}"
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# RABBITMQ
|
||||||
|
# ============================================
|
||||||
rabbitmq_host: str = "185.197.75.249"
|
rabbitmq_host: str = "185.197.75.249"
|
||||||
rabbitmq_port: int = 5672
|
rabbitmq_port: int = 5672
|
||||||
rabbitmq_user: str = "admin"
|
rabbitmq_user: str = "admin"
|
||||||
rabbitmq_password: str = "tyejvtej"
|
rabbitmq_password: str = "tyejvtej"
|
||||||
rabbitmq_vhost: str = "/"
|
rabbitmq_vhost: str = "/"
|
||||||
|
|
||||||
# OCR Service
|
@property
|
||||||
ocr_api_url: str = "http://147.45.146.17:8001"
|
def rabbitmq_url(self) -> str:
|
||||||
|
"""Формирует URL для подключения к RabbitMQ"""
|
||||||
|
return f"amqp://{self.rabbitmq_user}:{self.rabbitmq_password}@{self.rabbitmq_host}:{self.rabbitmq_port}{self.rabbitmq_vhost}"
|
||||||
|
|
||||||
# OpenRouter AI
|
# ============================================
|
||||||
|
# S3 STORAGE (Timeweb Cloud Storage)
|
||||||
|
# ============================================
|
||||||
|
s3_endpoint: str = "https://s3.timeweb.com"
|
||||||
|
s3_bucket: str = "erv-platform-files"
|
||||||
|
s3_access_key: str = "your_access_key_here"
|
||||||
|
s3_secret_key: str = "your_secret_key_here"
|
||||||
|
s3_region: str = "ru-1"
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# OCR SERVICE
|
||||||
|
# ============================================
|
||||||
|
ocr_api_url: str = "http://147.45.146.17:8001"
|
||||||
|
ocr_api_key: str = ""
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# AI SERVICE (OpenRouter)
|
||||||
|
# ============================================
|
||||||
openrouter_api_key: str = "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"
|
openrouter_api_key: str = "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"
|
||||||
|
openrouter_base_url: str = "https://openrouter.ai/api/v1"
|
||||||
openrouter_model: str = "google/gemini-2.0-flash-001"
|
openrouter_model: str = "google/gemini-2.0-flash-001"
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# FLIGHT APIs
|
||||||
|
# ============================================
|
||||||
# FlightAware
|
# FlightAware
|
||||||
flightaware_api_key: str = "Puz0cdxAHzAEqMRZwtdeqBUSm9naJfwK"
|
flightaware_api_key: str = "Puz0cdxAHzAEqMRZwtdeqBUSm9naJfwK"
|
||||||
|
flightaware_base_url: str = "https://aeroapi.flightaware.com/aeroapi"
|
||||||
|
|
||||||
|
# AviationStack (резервный)
|
||||||
|
aviationstack_api_key: str = ""
|
||||||
|
aviationstack_base_url: str = "http://api.aviationstack.com/v1"
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# NSPK BANKS API
|
||||||
|
# ============================================
|
||||||
|
nspk_banks_api_url: str = "https://qr.nspk.ru/proxyapp/c2bmembers.json"
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# SMS SERVICE (SigmaSMS)
|
||||||
|
# ============================================
|
||||||
|
sms_api_url: str = "https://online.sigmasms.ru/api/"
|
||||||
|
sms_login: str = ""
|
||||||
|
sms_password: str = ""
|
||||||
|
sms_token: str = ""
|
||||||
|
sms_sender: str = "lexpriority"
|
||||||
|
sms_enabled: bool = True
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# VTIGER CRM (PHP Bridge)
|
||||||
|
# ============================================
|
||||||
|
crm_webservice_url: str = "http://crm.clientright.ru/webservice.php"
|
||||||
|
crm_webform_url: str = "https://crm.clientright.ru/modules/Webforms/capture.php"
|
||||||
|
crm_token: str = ""
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# RATE LIMITING
|
||||||
|
# ============================================
|
||||||
|
rate_limit_per_minute: int = 60
|
||||||
|
rate_limit_per_hour: int = 1000
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# FILE UPLOAD
|
||||||
|
# ============================================
|
||||||
|
max_upload_size_mb: int = 50
|
||||||
|
allowed_file_extensions: str = "pdf,jpg,jpeg,png,heic,heif,webp"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def allowed_extensions_list(self) -> List[str]:
|
||||||
|
"""Список разрешенных расширений файлов"""
|
||||||
|
return [ext.strip() for ext in self.allowed_file_extensions.split(",")]
|
||||||
|
|
||||||
|
# ============================================
|
||||||
# CORS
|
# CORS
|
||||||
cors_origins: list = [
|
# ============================================
|
||||||
"http://localhost:5173",
|
cors_origins: str = "http://localhost:5173,http://147.45.146.17:5173,https://erv-claims.clientright.ru,http://crm.clientright.ru"
|
||||||
"http://147.45.146.17:5173",
|
|
||||||
"https://erv-claims.clientright.ru"
|
@property
|
||||||
]
|
def cors_origins_list(self) -> List[str]:
|
||||||
|
"""Список CORS origins"""
|
||||||
|
if isinstance(self.cors_origins, str):
|
||||||
|
return [origin.strip() for origin in self.cors_origins.split(",")]
|
||||||
|
return self.cors_origins
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# LOGGING
|
||||||
|
# ============================================
|
||||||
|
log_level: str = "INFO"
|
||||||
|
log_file: str = "/app/logs/erv_platform.log"
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
env_file = "../.env"
|
env_file = "../.env"
|
||||||
case_sensitive = False
|
case_sensitive = False
|
||||||
|
extra = "ignore" # Игнорируем лишние поля из .env
|
||||||
|
|
||||||
|
|
||||||
@lru_cache()
|
@lru_cache()
|
||||||
@@ -66,3 +166,4 @@ def get_settings() -> Settings:
|
|||||||
|
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,33 +3,84 @@ ERV Insurance Platform - FastAPI Backend
|
|||||||
"""
|
"""
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import JSONResponse
|
from contextlib import asynccontextmanager
|
||||||
from app.config import settings
|
import logging
|
||||||
import redis
|
|
||||||
import asyncpg
|
from .config import settings
|
||||||
|
from .services.database import db
|
||||||
|
from .services.redis_service import redis_service
|
||||||
|
from .services.rabbitmq_service import rabbitmq_service
|
||||||
|
from .api import sms, claims
|
||||||
|
|
||||||
|
# Настройка логирования
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
"""
|
||||||
|
Lifecycle events: startup and shutdown
|
||||||
|
"""
|
||||||
|
# STARTUP
|
||||||
|
logger.info("🚀 Starting ERV Platform...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Подключаем PostgreSQL
|
||||||
|
await db.connect()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"⚠️ PostgreSQL not available: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Подключаем Redis
|
||||||
|
await redis_service.connect()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"⚠️ Redis not available: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Подключаем RabbitMQ
|
||||||
|
await rabbitmq_service.connect()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"⚠️ RabbitMQ not available: {e}")
|
||||||
|
|
||||||
|
logger.info("✅ ERV Platform started successfully!")
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
# SHUTDOWN
|
||||||
|
logger.info("🛑 Shutting down ERV Platform...")
|
||||||
|
|
||||||
|
await db.disconnect()
|
||||||
|
await redis_service.disconnect()
|
||||||
|
await rabbitmq_service.disconnect()
|
||||||
|
|
||||||
|
logger.info("👋 ERV Platform stopped")
|
||||||
|
|
||||||
|
|
||||||
# Создаём FastAPI приложение
|
# Создаём FastAPI приложение
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="ERV Insurance Platform API",
|
title="ERV Insurance Platform API",
|
||||||
description="API для обработки страховых обращений с OCR, AI и интеграциями",
|
description="API для обработки страховых обращений",
|
||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
docs_url="/docs",
|
lifespan=lifespan
|
||||||
redoc_url="/redoc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# CORS middleware
|
# CORS
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=settings.cors_origins,
|
allow_origins=settings.cors_origins_list,
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# API Routes
|
||||||
|
app.include_router(sms.router)
|
||||||
|
app.include_router(claims.router)
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# HEALTH CHECKS
|
|
||||||
# ============================================
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
@@ -37,140 +88,111 @@ async def root():
|
|||||||
return {
|
return {
|
||||||
"message": "🚀 ERV Insurance Platform API",
|
"message": "🚀 ERV Insurance Platform API",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"docs": f"{settings.backend_url}/docs",
|
"status": "running",
|
||||||
"status": "running"
|
"docs": "http://147.45.146.17:8100/docs"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
async def health_check():
|
async def health():
|
||||||
"""Проверка здоровья сервисов"""
|
"""Health check - проверка всех сервисов"""
|
||||||
health_status = {
|
health_status = {
|
||||||
"api": "ok",
|
"status": "ok",
|
||||||
"redis": "checking",
|
"message": "API работает!",
|
||||||
"postgres": "checking",
|
"services": {}
|
||||||
"ocr": "checking"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Проверка Redis
|
|
||||||
try:
|
|
||||||
r = redis.Redis(
|
|
||||||
host=settings.redis_host,
|
|
||||||
port=settings.redis_port,
|
|
||||||
password=settings.redis_password,
|
|
||||||
decode_responses=True
|
|
||||||
)
|
|
||||||
r.ping()
|
|
||||||
health_status["redis"] = "ok"
|
|
||||||
except Exception as e:
|
|
||||||
health_status["redis"] = f"error: {str(e)}"
|
|
||||||
|
|
||||||
# Проверка PostgreSQL
|
# Проверка PostgreSQL
|
||||||
try:
|
try:
|
||||||
conn = await asyncpg.connect(
|
pg_healthy = await db.health_check()
|
||||||
host=settings.postgres_host,
|
health_status["services"]["postgresql"] = {
|
||||||
port=settings.postgres_port,
|
"status": "✅ healthy" if pg_healthy else "❌ unhealthy",
|
||||||
database=settings.postgres_db,
|
"connected": pg_healthy
|
||||||
user=settings.postgres_user,
|
|
||||||
password=settings.postgres_password
|
|
||||||
)
|
|
||||||
await conn.execute("SELECT 1")
|
|
||||||
await conn.close()
|
|
||||||
health_status["postgres"] = "ok"
|
|
||||||
except Exception as e:
|
|
||||||
health_status["postgres"] = f"error: {str(e)}"
|
|
||||||
|
|
||||||
# Проверка OCR
|
|
||||||
import httpx
|
|
||||||
try:
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.get(f"{settings.ocr_api_url}/", timeout=5.0)
|
|
||||||
health_status["ocr"] = "ok" if response.status_code in [200, 404] else "unreachable"
|
|
||||||
except Exception as e:
|
|
||||||
health_status["ocr"] = f"error: {str(e)}"
|
|
||||||
|
|
||||||
all_ok = all(v == "ok" for v in health_status.values())
|
|
||||||
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=200 if all_ok else 503,
|
|
||||||
content={
|
|
||||||
"status": "healthy" if all_ok else "degraded",
|
|
||||||
"services": health_status
|
|
||||||
}
|
}
|
||||||
|
except:
|
||||||
|
health_status["services"]["postgresql"] = {
|
||||||
|
"status": "❌ unavailable",
|
||||||
|
"connected": False
|
||||||
|
}
|
||||||
|
|
||||||
|
# Проверка Redis
|
||||||
|
try:
|
||||||
|
redis_healthy = await redis_service.health_check()
|
||||||
|
health_status["services"]["redis"] = {
|
||||||
|
"status": "✅ healthy" if redis_healthy else "❌ unhealthy",
|
||||||
|
"connected": redis_healthy
|
||||||
|
}
|
||||||
|
except:
|
||||||
|
health_status["services"]["redis"] = {
|
||||||
|
"status": "❌ unavailable",
|
||||||
|
"connected": False
|
||||||
|
}
|
||||||
|
|
||||||
|
# Проверка RabbitMQ
|
||||||
|
try:
|
||||||
|
rabbitmq_healthy = await rabbitmq_service.health_check()
|
||||||
|
health_status["services"]["rabbitmq"] = {
|
||||||
|
"status": "✅ healthy" if rabbitmq_healthy else "❌ unhealthy",
|
||||||
|
"connected": rabbitmq_healthy
|
||||||
|
}
|
||||||
|
except:
|
||||||
|
health_status["services"]["rabbitmq"] = {
|
||||||
|
"status": "❌ unavailable",
|
||||||
|
"connected": False
|
||||||
|
}
|
||||||
|
|
||||||
|
# Общий статус
|
||||||
|
all_healthy = all(
|
||||||
|
service.get("connected", False)
|
||||||
|
for service in health_status["services"].values()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not all_healthy:
|
||||||
|
health_status["status"] = "degraded"
|
||||||
|
health_status["message"] = "⚠️ Некоторые сервисы недоступны"
|
||||||
|
|
||||||
|
return health_status
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# API V1 ENDPOINTS
|
|
||||||
# ============================================
|
|
||||||
|
|
||||||
@app.get("/api/v1/test")
|
@app.get("/api/v1/test")
|
||||||
async def test_endpoint():
|
async def test():
|
||||||
"""Тестовый endpoint"""
|
"""Тестовый endpoint"""
|
||||||
return {
|
return {
|
||||||
"message": "✅ API работает!",
|
"success": True,
|
||||||
"env": settings.app_env,
|
"message": "✅ Backend API работает!",
|
||||||
"debug": settings.debug,
|
|
||||||
"services": {
|
"services": {
|
||||||
"redis": f"{settings.redis_host}:{settings.redis_port}",
|
"redis": "localhost:6379",
|
||||||
"postgres": f"{settings.postgres_host}:{settings.postgres_port}",
|
"postgres": "147.45.189.234:5432",
|
||||||
"rabbitmq": f"{settings.rabbitmq_host}:{settings.rabbitmq_port}",
|
"ocr": "147.45.146.17:8001",
|
||||||
"ocr": settings.ocr_api_url
|
"rabbitmq": "185.197.75.249:5672"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/v1/info")
|
@app.get("/api/v1/info")
|
||||||
async def get_info():
|
async def info():
|
||||||
"""Информация о платформе"""
|
"""Информация о платформе"""
|
||||||
return {
|
return {
|
||||||
"platform": "ERV Insurance Claims",
|
"platform": "ERV Insurance Claims",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"features": [
|
|
||||||
"OCR документов (паспорт, билеты)",
|
|
||||||
"AI автозаполнение (Gemini Vision)",
|
|
||||||
"Проверка рейсов (FlightAware)",
|
|
||||||
"СБП выплаты",
|
|
||||||
"Интеграция с CRM"
|
|
||||||
],
|
|
||||||
"tech_stack": {
|
"tech_stack": {
|
||||||
"backend": "Python FastAPI",
|
"backend": "Python FastAPI",
|
||||||
"frontend": "React TypeScript",
|
"frontend": "React TypeScript",
|
||||||
"database": "PostgreSQL + MySQL",
|
"database": "PostgreSQL + MySQL",
|
||||||
"cache": "Redis",
|
"cache": "Redis",
|
||||||
"queue": "RabbitMQ",
|
"queue": "RabbitMQ",
|
||||||
"storage": "S3 Timeweb",
|
"storage": "S3 Timeweb"
|
||||||
"ocr": "Internal Service",
|
},
|
||||||
"ai": "OpenRouter Gemini 2.0"
|
"features": [
|
||||||
}
|
"OCR документов (паспорт, билеты)",
|
||||||
|
"AI автозаполнение (Gemini Vision)",
|
||||||
|
"Проверка рейсов (FlightAware)",
|
||||||
|
"СБП выплаты",
|
||||||
|
"Интеграция с CRM Vtiger"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# STARTUP/SHUTDOWN
|
|
||||||
# ============================================
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
|
||||||
async def startup_event():
|
|
||||||
"""При старте приложения"""
|
|
||||||
print("🚀 ERV Insurance Platform запускается...")
|
|
||||||
print(f"📍 Backend URL: {settings.backend_url}")
|
|
||||||
print(f"📍 API Docs: {settings.backend_url}/docs")
|
|
||||||
print(f"🔗 Frontend URL: {settings.frontend_url}")
|
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("shutdown")
|
|
||||||
async def shutdown_event():
|
|
||||||
"""При остановке приложения"""
|
|
||||||
print("👋 ERV Insurance Platform остановлен")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(
|
uvicorn.run(app, host="0.0.0.0", port=8100)
|
||||||
"main:app",
|
|
||||||
host="0.0.0.0",
|
|
||||||
port=8100,
|
|
||||||
reload=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|||||||
4
backend/app/services/__init__.py
Normal file
4
backend/app/services/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
"""
|
||||||
|
ERV Platform Services
|
||||||
|
"""
|
||||||
|
|
||||||
76
backend/app/services/database.py
Normal file
76
backend/app/services/database.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
"""
|
||||||
|
PostgreSQL Database Service
|
||||||
|
"""
|
||||||
|
import asyncpg
|
||||||
|
from typing import Optional, Dict, Any, List
|
||||||
|
from ..config import settings
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseService:
|
||||||
|
"""Сервис для работы с PostgreSQL"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.pool: Optional[asyncpg.Pool] = None
|
||||||
|
|
||||||
|
async def connect(self):
|
||||||
|
"""Создает пул подключений к PostgreSQL"""
|
||||||
|
try:
|
||||||
|
self.pool = await asyncpg.create_pool(
|
||||||
|
host=settings.postgres_host,
|
||||||
|
port=settings.postgres_port,
|
||||||
|
database=settings.postgres_db,
|
||||||
|
user=settings.postgres_user,
|
||||||
|
password=settings.postgres_password,
|
||||||
|
min_size=5,
|
||||||
|
max_size=20,
|
||||||
|
command_timeout=60
|
||||||
|
)
|
||||||
|
logger.info(f"✅ PostgreSQL connected: {settings.postgres_host}:{settings.postgres_port}/{settings.postgres_db}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ PostgreSQL connection error: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def disconnect(self):
|
||||||
|
"""Закрывает пул подключений"""
|
||||||
|
if self.pool:
|
||||||
|
await self.pool.close()
|
||||||
|
logger.info("PostgreSQL pool closed")
|
||||||
|
|
||||||
|
async def execute(self, query: str, *args) -> str:
|
||||||
|
"""Выполняет SQL запрос без возврата данных"""
|
||||||
|
async with self.pool.acquire() as conn:
|
||||||
|
return await conn.execute(query, *args)
|
||||||
|
|
||||||
|
async def fetch_one(self, query: str, *args) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Возвращает одну запись"""
|
||||||
|
async with self.pool.acquire() as conn:
|
||||||
|
row = await conn.fetchrow(query, *args)
|
||||||
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
async def fetch_all(self, query: str, *args) -> List[Dict[str, Any]]:
|
||||||
|
"""Возвращает все записи"""
|
||||||
|
async with self.pool.acquire() as conn:
|
||||||
|
rows = await conn.fetch(query, *args)
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
async def fetch_val(self, query: str, *args):
|
||||||
|
"""Возвращает одно значение"""
|
||||||
|
async with self.pool.acquire() as conn:
|
||||||
|
return await conn.fetchval(query, *args)
|
||||||
|
|
||||||
|
async def health_check(self) -> bool:
|
||||||
|
"""Проверка здоровья БД"""
|
||||||
|
try:
|
||||||
|
result = await self.fetch_val("SELECT 1")
|
||||||
|
return result == 1
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Database health check failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Глобальный экземпляр
|
||||||
|
db = DatabaseService()
|
||||||
|
|
||||||
226
backend/app/services/rabbitmq_service.py
Normal file
226
backend/app/services/rabbitmq_service.py
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
"""
|
||||||
|
RabbitMQ Service для асинхронной обработки задач
|
||||||
|
"""
|
||||||
|
import aio_pika
|
||||||
|
from aio_pika import Connection, Channel, Queue, Exchange, Message
|
||||||
|
from aio_pika.pool import Pool
|
||||||
|
from typing import Optional, Callable, Dict, Any
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from ..config import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RabbitMQService:
|
||||||
|
"""Сервис для работы с RabbitMQ"""
|
||||||
|
|
||||||
|
# Названия очередей
|
||||||
|
QUEUE_OCR_PROCESSING = "erv_ocr_processing"
|
||||||
|
QUEUE_AI_EXTRACTION = "erv_ai_extraction"
|
||||||
|
QUEUE_FLIGHT_CHECK = "erv_flight_check"
|
||||||
|
QUEUE_CRM_INTEGRATION = "erv_crm_integration"
|
||||||
|
QUEUE_NOTIFICATIONS = "erv_notifications"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.connection: Optional[Connection] = None
|
||||||
|
self.channel: Optional[Channel] = None
|
||||||
|
self.queues: Dict[str, Queue] = {}
|
||||||
|
|
||||||
|
async def connect(self):
|
||||||
|
"""Подключение к RabbitMQ"""
|
||||||
|
try:
|
||||||
|
self.connection = await aio_pika.connect_robust(
|
||||||
|
settings.rabbitmq_url,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
self.channel = await self.connection.channel()
|
||||||
|
await self.channel.set_qos(prefetch_count=10)
|
||||||
|
|
||||||
|
logger.info(f"✅ RabbitMQ connected: {settings.rabbitmq_host}:{settings.rabbitmq_port}")
|
||||||
|
|
||||||
|
# Объявляем очереди
|
||||||
|
await self._declare_queues()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ RabbitMQ connection error: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def disconnect(self):
|
||||||
|
"""Отключение от RabbitMQ"""
|
||||||
|
if self.connection:
|
||||||
|
await self.connection.close()
|
||||||
|
logger.info("RabbitMQ connection closed")
|
||||||
|
|
||||||
|
async def _declare_queues(self):
|
||||||
|
"""Объявляем все рабочие очереди"""
|
||||||
|
queue_names = [
|
||||||
|
self.QUEUE_OCR_PROCESSING,
|
||||||
|
self.QUEUE_AI_EXTRACTION,
|
||||||
|
self.QUEUE_FLIGHT_CHECK,
|
||||||
|
self.QUEUE_CRM_INTEGRATION,
|
||||||
|
self.QUEUE_NOTIFICATIONS,
|
||||||
|
]
|
||||||
|
|
||||||
|
for queue_name in queue_names:
|
||||||
|
queue = await self.channel.declare_queue(
|
||||||
|
queue_name,
|
||||||
|
durable=True, # Очередь переживет перезапуск
|
||||||
|
arguments={
|
||||||
|
"x-message-ttl": 3600000, # TTL сообщений 1 час
|
||||||
|
"x-max-length": 10000, # Максимум сообщений в очереди
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.queues[queue_name] = queue
|
||||||
|
logger.info(f"✅ Queue declared: {queue_name}")
|
||||||
|
|
||||||
|
async def publish(
|
||||||
|
self,
|
||||||
|
queue_name: str,
|
||||||
|
message: Dict[str, Any],
|
||||||
|
priority: int = 5,
|
||||||
|
headers: Optional[Dict[str, Any]] = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Публикация сообщения в очередь
|
||||||
|
|
||||||
|
Args:
|
||||||
|
queue_name: Название очереди
|
||||||
|
message: Данные сообщения (dict)
|
||||||
|
priority: Приоритет (0-10, где 10 - максимальный)
|
||||||
|
headers: Дополнительные заголовки
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
msg_body = json.dumps(message).encode()
|
||||||
|
|
||||||
|
msg = Message(
|
||||||
|
body=msg_body,
|
||||||
|
priority=priority,
|
||||||
|
headers=headers or {},
|
||||||
|
content_type="application/json",
|
||||||
|
delivery_mode=aio_pika.DeliveryMode.PERSISTENT # Сохранять на диск
|
||||||
|
)
|
||||||
|
|
||||||
|
# Публикуем в default exchange с routing_key = queue_name
|
||||||
|
await self.channel.default_exchange.publish(
|
||||||
|
msg,
|
||||||
|
routing_key=queue_name
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(f"📤 Message published to {queue_name}: {message.get('task_id', 'unknown')}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Failed to publish message to {queue_name}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def consume(
|
||||||
|
self,
|
||||||
|
queue_name: str,
|
||||||
|
callback: Callable,
|
||||||
|
prefetch_count: int = 1
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Подписка на сообщения из очереди
|
||||||
|
|
||||||
|
Args:
|
||||||
|
queue_name: Название очереди
|
||||||
|
callback: Асинхронная функция-обработчик
|
||||||
|
prefetch_count: Количество сообщений для одновременной обработки
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
queue = self.queues.get(queue_name)
|
||||||
|
if not queue:
|
||||||
|
logger.error(f"Queue {queue_name} not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.channel.set_qos(prefetch_count=prefetch_count)
|
||||||
|
|
||||||
|
await queue.consume(callback)
|
||||||
|
logger.info(f"👂 Consuming from {queue_name}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Failed to consume from {queue_name}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def health_check(self) -> bool:
|
||||||
|
"""Проверка здоровья RabbitMQ"""
|
||||||
|
try:
|
||||||
|
if self.connection and not self.connection.is_closed:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"RabbitMQ health check failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ДЛЯ ЗАДАЧ
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
async def publish_ocr_task(self, claim_id: str, file_id: str, file_path: str):
|
||||||
|
"""Отправка задачи на OCR обработку"""
|
||||||
|
await self.publish(
|
||||||
|
self.QUEUE_OCR_PROCESSING,
|
||||||
|
{
|
||||||
|
"task_type": "ocr_processing",
|
||||||
|
"claim_id": claim_id,
|
||||||
|
"file_id": file_id,
|
||||||
|
"file_path": file_path
|
||||||
|
},
|
||||||
|
priority=8
|
||||||
|
)
|
||||||
|
|
||||||
|
async def publish_ai_extraction_task(self, claim_id: str, file_id: str, ocr_text: str):
|
||||||
|
"""Отправка задачи на AI извлечение данных"""
|
||||||
|
await self.publish(
|
||||||
|
self.QUEUE_AI_EXTRACTION,
|
||||||
|
{
|
||||||
|
"task_type": "ai_extraction",
|
||||||
|
"claim_id": claim_id,
|
||||||
|
"file_id": file_id,
|
||||||
|
"ocr_text": ocr_text
|
||||||
|
},
|
||||||
|
priority=7
|
||||||
|
)
|
||||||
|
|
||||||
|
async def publish_flight_check_task(self, claim_id: str, flight_number: str, flight_date: str):
|
||||||
|
"""Отправка задачи на проверку рейса"""
|
||||||
|
await self.publish(
|
||||||
|
self.QUEUE_FLIGHT_CHECK,
|
||||||
|
{
|
||||||
|
"task_type": "flight_check",
|
||||||
|
"claim_id": claim_id,
|
||||||
|
"flight_number": flight_number,
|
||||||
|
"flight_date": flight_date
|
||||||
|
},
|
||||||
|
priority=6
|
||||||
|
)
|
||||||
|
|
||||||
|
async def publish_crm_integration_task(self, claim_id: str, form_data: Dict[str, Any]):
|
||||||
|
"""Отправка задачи на интеграцию с CRM"""
|
||||||
|
await self.publish(
|
||||||
|
self.QUEUE_CRM_INTEGRATION,
|
||||||
|
{
|
||||||
|
"task_type": "crm_integration",
|
||||||
|
"claim_id": claim_id,
|
||||||
|
"form_data": form_data
|
||||||
|
},
|
||||||
|
priority=9 # Высокий приоритет
|
||||||
|
)
|
||||||
|
|
||||||
|
async def publish_notification_task(self, claim_id: str, notification_type: str, data: Dict[str, Any]):
|
||||||
|
"""Отправка задачи на отправку уведомления"""
|
||||||
|
await self.publish(
|
||||||
|
self.QUEUE_NOTIFICATIONS,
|
||||||
|
{
|
||||||
|
"task_type": "notification",
|
||||||
|
"claim_id": claim_id,
|
||||||
|
"notification_type": notification_type,
|
||||||
|
"data": data
|
||||||
|
},
|
||||||
|
priority=5
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Глобальный экземпляр
|
||||||
|
rabbitmq_service = RabbitMQService()
|
||||||
|
|
||||||
146
backend/app/services/redis_service.py
Normal file
146
backend/app/services/redis_service.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
"""
|
||||||
|
Redis Service для кеширования, rate limiting, сессий
|
||||||
|
"""
|
||||||
|
import redis.asyncio as redis
|
||||||
|
from typing import Optional, Any
|
||||||
|
import json
|
||||||
|
from ..config import settings
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RedisService:
|
||||||
|
"""Сервис для работы с Redis"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.client: Optional[redis.Redis] = None
|
||||||
|
|
||||||
|
async def connect(self):
|
||||||
|
"""Подключение к Redis"""
|
||||||
|
try:
|
||||||
|
self.client = await redis.from_url(
|
||||||
|
settings.redis_url,
|
||||||
|
encoding="utf-8",
|
||||||
|
decode_responses=True
|
||||||
|
)
|
||||||
|
await self.client.ping()
|
||||||
|
logger.info(f"✅ Redis connected: {settings.redis_host}:{settings.redis_port}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Redis connection error: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def disconnect(self):
|
||||||
|
"""Отключение от Redis"""
|
||||||
|
if self.client:
|
||||||
|
await self.client.close()
|
||||||
|
logger.info("Redis connection closed")
|
||||||
|
|
||||||
|
async def get(self, key: str) -> Optional[str]:
|
||||||
|
"""Получить значение по ключу"""
|
||||||
|
full_key = f"{settings.redis_prefix}{key}"
|
||||||
|
return await self.client.get(full_key)
|
||||||
|
|
||||||
|
async def set(self, key: str, value: Any, expire: Optional[int] = None):
|
||||||
|
"""Установить значение с опциональным TTL (в секундах)"""
|
||||||
|
full_key = f"{settings.redis_prefix}{key}"
|
||||||
|
if isinstance(value, (dict, list)):
|
||||||
|
value = json.dumps(value)
|
||||||
|
if expire:
|
||||||
|
await self.client.setex(full_key, expire, value)
|
||||||
|
else:
|
||||||
|
await self.client.set(full_key, value)
|
||||||
|
|
||||||
|
async def delete(self, key: str) -> bool:
|
||||||
|
"""Удалить ключ"""
|
||||||
|
full_key = f"{settings.redis_prefix}{key}"
|
||||||
|
result = await self.client.delete(full_key)
|
||||||
|
return result > 0
|
||||||
|
|
||||||
|
async def exists(self, key: str) -> bool:
|
||||||
|
"""Проверить существование ключа"""
|
||||||
|
full_key = f"{settings.redis_prefix}{key}"
|
||||||
|
return await self.client.exists(full_key) > 0
|
||||||
|
|
||||||
|
async def increment(self, key: str, amount: int = 1) -> int:
|
||||||
|
"""Инкремент значения"""
|
||||||
|
full_key = f"{settings.redis_prefix}{key}"
|
||||||
|
return await self.client.incrby(full_key, amount)
|
||||||
|
|
||||||
|
async def expire(self, key: str, seconds: int):
|
||||||
|
"""Установить TTL для ключа"""
|
||||||
|
full_key = f"{settings.redis_prefix}{key}"
|
||||||
|
await self.client.expire(full_key, seconds)
|
||||||
|
|
||||||
|
async def get_json(self, key: str) -> Optional[dict]:
|
||||||
|
"""Получить JSON значение"""
|
||||||
|
value = await self.get(key)
|
||||||
|
if value:
|
||||||
|
try:
|
||||||
|
return json.loads(value)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def set_json(self, key: str, value: dict, expire: Optional[int] = None):
|
||||||
|
"""Установить JSON значение"""
|
||||||
|
await self.set(key, json.dumps(value), expire)
|
||||||
|
|
||||||
|
async def health_check(self) -> bool:
|
||||||
|
"""Проверка здоровья Redis"""
|
||||||
|
try:
|
||||||
|
return await self.client.ping()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Redis health check failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# RATE LIMITING
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
async def check_rate_limit(self, identifier: str, max_requests: int, window_seconds: int) -> tuple[bool, int]:
|
||||||
|
"""
|
||||||
|
Проверка rate limiting
|
||||||
|
Returns: (allowed: bool, remaining: int)
|
||||||
|
"""
|
||||||
|
key = f"ratelimit:{identifier}"
|
||||||
|
full_key = f"{settings.redis_prefix}{key}"
|
||||||
|
|
||||||
|
current = await self.client.get(full_key)
|
||||||
|
|
||||||
|
if current is None:
|
||||||
|
# Первый запрос в окне
|
||||||
|
await self.client.setex(full_key, window_seconds, 1)
|
||||||
|
return True, max_requests - 1
|
||||||
|
|
||||||
|
current_count = int(current)
|
||||||
|
|
||||||
|
if current_count >= max_requests:
|
||||||
|
# Лимит превышен
|
||||||
|
ttl = await self.client.ttl(full_key)
|
||||||
|
return False, 0
|
||||||
|
|
||||||
|
# Инкремент счетчика
|
||||||
|
new_count = await self.client.incr(full_key)
|
||||||
|
return True, max_requests - new_count
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# CACHE
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
async def cache_get(self, cache_key: str) -> Optional[Any]:
|
||||||
|
"""Получить из кеша"""
|
||||||
|
return await self.get_json(f"cache:{cache_key}")
|
||||||
|
|
||||||
|
async def cache_set(self, cache_key: str, value: Any, ttl: int = 3600):
|
||||||
|
"""Сохранить в кеш (TTL по умолчанию 1 час)"""
|
||||||
|
await self.set_json(f"cache:{cache_key}", value, ttl)
|
||||||
|
|
||||||
|
async def cache_delete(self, cache_key: str):
|
||||||
|
"""Удалить из кеша"""
|
||||||
|
await self.delete(f"cache:{cache_key}")
|
||||||
|
|
||||||
|
|
||||||
|
# Глобальный экземпляр
|
||||||
|
redis_service = RedisService()
|
||||||
|
|
||||||
180
backend/app/services/sms_service.py
Normal file
180
backend/app/services/sms_service.py
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
"""
|
||||||
|
SMS Service для отправки кодов верификации (SigmaSMS)
|
||||||
|
"""
|
||||||
|
import httpx
|
||||||
|
import random
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
from ..config import settings
|
||||||
|
from .redis_service import redis_service
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SMSService:
|
||||||
|
"""Сервис для работы с SMS через SigmaSMS API"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.api_url = settings.sms_api_url
|
||||||
|
self.login = settings.sms_login
|
||||||
|
self.password = settings.sms_password
|
||||||
|
self.token = settings.sms_token
|
||||||
|
self.sender = settings.sms_sender
|
||||||
|
self.enabled = settings.sms_enabled
|
||||||
|
|
||||||
|
async def _get_token(self) -> Optional[str]:
|
||||||
|
"""Получить JWT токен для API"""
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.post(
|
||||||
|
f"{self.api_url}login",
|
||||||
|
json={
|
||||||
|
"username": self.login,
|
||||||
|
"password": self.password
|
||||||
|
},
|
||||||
|
timeout=10.0
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
return data.get("token")
|
||||||
|
else:
|
||||||
|
logger.error(f"Failed to get SMS token: {response.status_code}")
|
||||||
|
return self.token # Используем токен из .env как fallback
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting SMS token: {e}")
|
||||||
|
return self.token
|
||||||
|
|
||||||
|
def generate_code(self) -> str:
|
||||||
|
"""Генерировать 6-значный код"""
|
||||||
|
return str(random.randint(100000, 999999))
|
||||||
|
|
||||||
|
async def send_sms(self, phone: str, message: str) -> bool:
|
||||||
|
"""
|
||||||
|
Отправить SMS
|
||||||
|
|
||||||
|
Args:
|
||||||
|
phone: Номер телефона (формат: +79001234567)
|
||||||
|
message: Текст сообщения
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если отправлено успешно
|
||||||
|
"""
|
||||||
|
if not self.enabled:
|
||||||
|
logger.warning("SMS отправка отключена в конфигурации")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Получаем актуальный токен
|
||||||
|
token = await self._get_token()
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
logger.error("No SMS token available")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Очищаем номер телефона
|
||||||
|
clean_phone = phone.replace("+", "").replace("-", "").replace(" ", "")
|
||||||
|
|
||||||
|
# Отправляем SMS
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.post(
|
||||||
|
f"{self.api_url}sendings",
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {token}"
|
||||||
|
},
|
||||||
|
json={
|
||||||
|
"recipient": clean_phone,
|
||||||
|
"type": "sms",
|
||||||
|
"payload": {
|
||||||
|
"sender": self.sender,
|
||||||
|
"text": message
|
||||||
|
}
|
||||||
|
},
|
||||||
|
timeout=15.0
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code in [200, 201]:
|
||||||
|
logger.info(f"✅ SMS sent to {phone}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"Failed to send SMS: {response.status_code} - {response.text}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error sending SMS: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def send_verification_code(self, phone: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Отправить код верификации на телефон
|
||||||
|
|
||||||
|
Args:
|
||||||
|
phone: Номер телефона
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Код верификации (для отладки) или None при ошибке
|
||||||
|
"""
|
||||||
|
# Проверка rate limiting (не больше 1 SMS в минуту на номер)
|
||||||
|
rate_limit_key = f"sms_rate:{phone}"
|
||||||
|
|
||||||
|
if await redis_service.exists(rate_limit_key):
|
||||||
|
ttl = await redis_service.client.ttl(f"{settings.redis_prefix}{rate_limit_key}")
|
||||||
|
logger.warning(f"Rate limit for {phone}, retry in {ttl} seconds")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Генерируем код
|
||||||
|
code = self.generate_code()
|
||||||
|
|
||||||
|
# Сохраняем код в Redis на 10 минут
|
||||||
|
verification_key = f"sms_verify:{phone}"
|
||||||
|
await redis_service.set(verification_key, code, expire=600) # 10 минут
|
||||||
|
|
||||||
|
# Устанавливаем rate limit на 60 секунд
|
||||||
|
await redis_service.set(rate_limit_key, "1", expire=60)
|
||||||
|
|
||||||
|
# Формируем сообщение
|
||||||
|
message = f"Ваш код подтверждения: {code}. Действителен 10 минут."
|
||||||
|
|
||||||
|
# Отправляем SMS
|
||||||
|
success = await self.send_sms(phone, message)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.info(f"Verification code sent to {phone}")
|
||||||
|
return code # Возвращаем для отладки
|
||||||
|
else:
|
||||||
|
# Удаляем код если не удалось отправить
|
||||||
|
await redis_service.delete(verification_key)
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def verify_code(self, phone: str, code: str) -> bool:
|
||||||
|
"""
|
||||||
|
Проверить код верификации
|
||||||
|
|
||||||
|
Args:
|
||||||
|
phone: Номер телефона
|
||||||
|
code: Код для проверки
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если код верный
|
||||||
|
"""
|
||||||
|
verification_key = f"sms_verify:{phone}"
|
||||||
|
stored_code = await redis_service.get(verification_key)
|
||||||
|
|
||||||
|
if not stored_code:
|
||||||
|
logger.warning(f"No verification code found for {phone}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if stored_code == code:
|
||||||
|
# Удаляем код после успешной проверки
|
||||||
|
await redis_service.delete(verification_key)
|
||||||
|
logger.info(f"✅ Code verified for {phone}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning(f"Invalid code for {phone}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Глобальный экземпляр
|
||||||
|
sms_service = SMSService()
|
||||||
|
|
||||||
306
backend/db/init.sql
Normal file
306
backend/db/init.sql
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
-- ERV Platform Database Initialization Script
|
||||||
|
-- PostgreSQL 16+
|
||||||
|
|
||||||
|
-- Enable extensions
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- ТАБЛИЦА: claims (Основные заявки)
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS claims (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
claim_number VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
|
||||||
|
-- Тип страхования
|
||||||
|
insurance_type VARCHAR(50) NOT NULL DEFAULT 'erv_travel',
|
||||||
|
case_type VARCHAR(100), -- например: 'flight_delay', 'medical', 'baggage_loss'
|
||||||
|
|
||||||
|
-- Данные клиента
|
||||||
|
client_phone VARCHAR(20) NOT NULL,
|
||||||
|
client_email VARCHAR(255),
|
||||||
|
client_inn VARCHAR(12),
|
||||||
|
client_full_name VARCHAR(500),
|
||||||
|
|
||||||
|
-- Данные полиса
|
||||||
|
policy_number VARCHAR(100),
|
||||||
|
policy_series VARCHAR(50),
|
||||||
|
|
||||||
|
-- Статус обработки
|
||||||
|
status VARCHAR(50) NOT NULL DEFAULT 'draft', -- draft, processing, crm_sent, completed, error
|
||||||
|
crm_id VARCHAR(100), -- ID в Vtiger CRM
|
||||||
|
|
||||||
|
-- Данные для аналитики
|
||||||
|
source VARCHAR(100), -- откуда пришла заявка: 'web_form', 'api', 'mobile_app'
|
||||||
|
user_agent TEXT,
|
||||||
|
ip_address INET,
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
submitted_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
completed_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
|
||||||
|
-- JSON поля для гибкости
|
||||||
|
form_data JSONB, -- все данные формы
|
||||||
|
metadata JSONB -- дополнительные метаданные
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Индексы для claims
|
||||||
|
CREATE INDEX idx_claims_claim_number ON claims(claim_number);
|
||||||
|
CREATE INDEX idx_claims_status ON claims(status);
|
||||||
|
CREATE INDEX idx_claims_created_at ON claims(created_at DESC);
|
||||||
|
CREATE INDEX idx_claims_client_phone ON claims(client_phone);
|
||||||
|
CREATE INDEX idx_claims_policy_number ON claims(policy_number);
|
||||||
|
CREATE INDEX idx_claims_insurance_type ON claims(insurance_type);
|
||||||
|
CREATE INDEX idx_claims_form_data_gin ON claims USING gin(form_data);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- ТАБЛИЦА: claim_files (Файлы к заявкам)
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS claim_files (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
claim_id UUID NOT NULL REFERENCES claims(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Файл
|
||||||
|
file_name VARCHAR(255) NOT NULL,
|
||||||
|
file_path VARCHAR(500) NOT NULL,
|
||||||
|
file_size BIGINT,
|
||||||
|
mime_type VARCHAR(100),
|
||||||
|
file_type VARCHAR(50), -- 'passport', 'ticket', 'receipt', 'medical_doc', etc.
|
||||||
|
|
||||||
|
-- S3 данные (если используется)
|
||||||
|
s3_bucket VARCHAR(255),
|
||||||
|
s3_key VARCHAR(500),
|
||||||
|
s3_url TEXT,
|
||||||
|
|
||||||
|
-- OCR/AI обработка
|
||||||
|
ocr_status VARCHAR(50), -- 'pending', 'processing', 'completed', 'error'
|
||||||
|
ocr_text TEXT,
|
||||||
|
ai_extracted_data JSONB,
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
processed_at TIMESTAMP WITH TIME ZONE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Индексы для claim_files
|
||||||
|
CREATE INDEX idx_claim_files_claim_id ON claim_files(claim_id);
|
||||||
|
CREATE INDEX idx_claim_files_file_type ON claim_files(file_type);
|
||||||
|
CREATE INDEX idx_claim_files_ocr_status ON claim_files(ocr_status);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- ТАБЛИЦА: processing_logs (Логи обработки)
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS processing_logs (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
claim_id UUID REFERENCES claims(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Лог
|
||||||
|
level VARCHAR(20) NOT NULL, -- 'debug', 'info', 'warning', 'error', 'critical'
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
context JSONB, -- дополнительный контекст
|
||||||
|
|
||||||
|
-- Источник
|
||||||
|
source VARCHAR(100), -- 'ocr_service', 'ai_service', 'flight_api', 'crm_integration', etc.
|
||||||
|
function_name VARCHAR(200),
|
||||||
|
|
||||||
|
-- Ошибки
|
||||||
|
error_type VARCHAR(100),
|
||||||
|
error_traceback TEXT,
|
||||||
|
|
||||||
|
-- Timestamp
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Индексы для processing_logs
|
||||||
|
CREATE INDEX idx_processing_logs_claim_id ON processing_logs(claim_id);
|
||||||
|
CREATE INDEX idx_processing_logs_level ON processing_logs(level);
|
||||||
|
CREATE INDEX idx_processing_logs_created_at ON processing_logs(created_at DESC);
|
||||||
|
CREATE INDEX idx_processing_logs_source ON processing_logs(source);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- ТАБЛИЦА: api_calls (Логи внешних API вызовов)
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS api_calls (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
claim_id UUID REFERENCES claims(id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
-- API детали
|
||||||
|
api_name VARCHAR(100) NOT NULL, -- 'ocr_service', 'openrouter_ai', 'flightaware', 'nspk_banks', etc.
|
||||||
|
endpoint VARCHAR(500),
|
||||||
|
method VARCHAR(10), -- 'GET', 'POST', etc.
|
||||||
|
|
||||||
|
-- Запрос
|
||||||
|
request_headers JSONB,
|
||||||
|
request_body JSONB,
|
||||||
|
|
||||||
|
-- Ответ
|
||||||
|
response_status INTEGER,
|
||||||
|
response_headers JSONB,
|
||||||
|
response_body JSONB,
|
||||||
|
|
||||||
|
-- Производительность
|
||||||
|
duration_ms INTEGER, -- длительность запроса в миллисекундах
|
||||||
|
|
||||||
|
-- Результат
|
||||||
|
success BOOLEAN DEFAULT FALSE,
|
||||||
|
error_message TEXT,
|
||||||
|
|
||||||
|
-- Timestamp
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Индексы для api_calls
|
||||||
|
CREATE INDEX idx_api_calls_claim_id ON api_calls(claim_id);
|
||||||
|
CREATE INDEX idx_api_calls_api_name ON api_calls(api_name);
|
||||||
|
CREATE INDEX idx_api_calls_success ON api_calls(success);
|
||||||
|
CREATE INDEX idx_api_calls_created_at ON api_calls(created_at DESC);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- ТАБЛИЦА: queue_tasks (Задачи в очереди RabbitMQ)
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS queue_tasks (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
claim_id UUID REFERENCES claims(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Задача
|
||||||
|
task_type VARCHAR(100) NOT NULL, -- 'ocr_processing', 'ai_extraction', 'flight_check', etc.
|
||||||
|
queue_name VARCHAR(100) NOT NULL,
|
||||||
|
priority INTEGER DEFAULT 5, -- 1-10, где 10 - максимальный приоритет
|
||||||
|
|
||||||
|
-- Данные
|
||||||
|
task_data JSONB,
|
||||||
|
|
||||||
|
-- Статус
|
||||||
|
status VARCHAR(50) NOT NULL DEFAULT 'pending', -- pending, processing, completed, failed, retry
|
||||||
|
attempts INTEGER DEFAULT 0,
|
||||||
|
max_attempts INTEGER DEFAULT 3,
|
||||||
|
|
||||||
|
-- Результат
|
||||||
|
result JSONB,
|
||||||
|
error_message TEXT,
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
started_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
completed_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
next_retry_at TIMESTAMP WITH TIME ZONE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Индексы для queue_tasks
|
||||||
|
CREATE INDEX idx_queue_tasks_claim_id ON queue_tasks(claim_id);
|
||||||
|
CREATE INDEX idx_queue_tasks_status ON queue_tasks(status);
|
||||||
|
CREATE INDEX idx_queue_tasks_task_type ON queue_tasks(task_type);
|
||||||
|
CREATE INDEX idx_queue_tasks_created_at ON queue_tasks(created_at DESC);
|
||||||
|
CREATE INDEX idx_queue_tasks_next_retry ON queue_tasks(next_retry_at) WHERE status = 'retry';
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- ТАБЛИЦА: metrics (Метрики системы)
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS metrics (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
|
||||||
|
-- Метрика
|
||||||
|
metric_name VARCHAR(100) NOT NULL,
|
||||||
|
metric_value NUMERIC(20, 4),
|
||||||
|
metric_unit VARCHAR(50), -- 'ms', 'count', 'bytes', '%', etc.
|
||||||
|
|
||||||
|
-- Теги для группировки
|
||||||
|
tags JSONB,
|
||||||
|
|
||||||
|
-- Timestamp
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Индексы для metrics
|
||||||
|
CREATE INDEX idx_metrics_metric_name ON metrics(metric_name);
|
||||||
|
CREATE INDEX idx_metrics_created_at ON metrics(created_at DESC);
|
||||||
|
CREATE INDEX idx_metrics_tags_gin ON metrics USING gin(tags);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- ТАБЛИЦА: cache_entries (Кеш для редких запросов)
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS cache_entries (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
|
||||||
|
-- Ключ-значение
|
||||||
|
cache_key VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
cache_value JSONB,
|
||||||
|
|
||||||
|
-- TTL
|
||||||
|
expires_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Индексы для cache_entries
|
||||||
|
CREATE INDEX idx_cache_entries_key ON cache_entries(cache_key);
|
||||||
|
CREATE INDEX idx_cache_entries_expires ON cache_entries(expires_at);
|
||||||
|
|
||||||
|
-- Автоматическая очистка устаревшего кеша
|
||||||
|
CREATE OR REPLACE FUNCTION cleanup_expired_cache()
|
||||||
|
RETURNS void AS $$
|
||||||
|
BEGIN
|
||||||
|
DELETE FROM cache_entries WHERE expires_at < CURRENT_TIMESTAMP;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- ТРИГГЕРЫ
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- Автоматическое обновление updated_at для claims
|
||||||
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER update_claims_updated_at
|
||||||
|
BEFORE UPDATE ON claims
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- ПРЕДСТАВЛЕНИЯ (VIEWS)
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- Статистика по заявкам
|
||||||
|
CREATE OR REPLACE VIEW claims_statistics AS
|
||||||
|
SELECT
|
||||||
|
DATE(created_at) as date,
|
||||||
|
insurance_type,
|
||||||
|
status,
|
||||||
|
COUNT(*) as count,
|
||||||
|
AVG(EXTRACT(EPOCH FROM (completed_at - created_at))) as avg_processing_time_seconds
|
||||||
|
FROM claims
|
||||||
|
GROUP BY DATE(created_at), insurance_type, status;
|
||||||
|
|
||||||
|
-- Статистика по API вызовам
|
||||||
|
CREATE OR REPLACE VIEW api_performance AS
|
||||||
|
SELECT
|
||||||
|
api_name,
|
||||||
|
DATE(created_at) as date,
|
||||||
|
COUNT(*) as total_calls,
|
||||||
|
SUM(CASE WHEN success THEN 1 ELSE 0 END) as successful_calls,
|
||||||
|
AVG(duration_ms) as avg_duration_ms,
|
||||||
|
MAX(duration_ms) as max_duration_ms
|
||||||
|
FROM api_calls
|
||||||
|
GROUP BY api_name, DATE(created_at);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- НАЧАЛЬНЫЕ ДАННЫЕ
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- Можно добавить тестовые данные для разработки
|
||||||
|
-- INSERT INTO claims (claim_number, client_phone, insurance_type, status)
|
||||||
|
-- VALUES ('TEST-001', '+79001234567', 'erv_travel', 'draft');
|
||||||
|
|
||||||
|
-- Завершение
|
||||||
|
SELECT 'Database initialized successfully!' as message;
|
||||||
|
|
||||||
137
docker-compose.full.yml
Normal file
137
docker-compose.full.yml
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# PostgreSQL для логов, метрик, аналитики
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: erv_postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: erv_platform
|
||||||
|
POSTGRES_USER: erv_user
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-erv_secure_pass_2024}
|
||||||
|
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C"
|
||||||
|
ports:
|
||||||
|
- "5433:5432" # 5433 чтобы не конфликтовать с системным PostgreSQL
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./backend/db/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
networks:
|
||||||
|
- erv_network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U erv_user -d erv_platform"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Redis для кеширования, сессий, rate limiting
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: erv_redis
|
||||||
|
restart: unless-stopped
|
||||||
|
command: redis-server --requirepass ${REDIS_PASSWORD:-redis_secure_pass_2024} --appendonly yes
|
||||||
|
ports:
|
||||||
|
- "6380:6379" # 6380 чтобы не конфликтовать с системным Redis
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
networks:
|
||||||
|
- erv_network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# pgAdmin для управления PostgreSQL (опционально)
|
||||||
|
pgadmin:
|
||||||
|
image: dpage/pgadmin4:latest
|
||||||
|
container_name: erv_pgadmin
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL:-admin@erv.local}
|
||||||
|
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:-admin}
|
||||||
|
PGADMIN_LISTEN_PORT: 80
|
||||||
|
ports:
|
||||||
|
- "5050:80"
|
||||||
|
volumes:
|
||||||
|
- pgadmin_data:/var/lib/pgadmin
|
||||||
|
networks:
|
||||||
|
- erv_network
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
|
||||||
|
# FastAPI Backend
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: erv_backend
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
# Database
|
||||||
|
POSTGRES_HOST: postgres
|
||||||
|
POSTGRES_PORT: 5432
|
||||||
|
POSTGRES_DB: erv_platform
|
||||||
|
POSTGRES_USER: erv_user
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-erv_secure_pass_2024}
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_HOST: redis
|
||||||
|
REDIS_PORT: 6379
|
||||||
|
REDIS_PASSWORD: ${REDIS_PASSWORD:-redis_secure_pass_2024}
|
||||||
|
|
||||||
|
# RabbitMQ (внешний)
|
||||||
|
RABBITMQ_HOST: ${RABBITMQ_HOST:-185.197.75.249}
|
||||||
|
RABBITMQ_PORT: ${RABBITMQ_PORT:-5672}
|
||||||
|
RABBITMQ_USER: ${RABBITMQ_USER:-admin}
|
||||||
|
RABBITMQ_PASSWORD: ${RABBITMQ_PASSWORD:-tyejvtej}
|
||||||
|
|
||||||
|
# API URLs
|
||||||
|
OCR_SERVICE_URL: ${OCR_SERVICE_URL:-http://147.45.146.17:8001}
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- "8100:8000"
|
||||||
|
volumes:
|
||||||
|
- ./backend/app:/app/app
|
||||||
|
- ./backend/logs:/app/logs
|
||||||
|
- uploads:/app/uploads
|
||||||
|
networks:
|
||||||
|
- erv_network
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
# React Frontend
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: erv_frontend
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "5173:3000"
|
||||||
|
environment:
|
||||||
|
- VITE_API_URL=http://147.45.146.17:8100
|
||||||
|
networks:
|
||||||
|
- erv_network
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
networks:
|
||||||
|
erv_network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
driver: local
|
||||||
|
redis_data:
|
||||||
|
driver: local
|
||||||
|
pgadmin_data:
|
||||||
|
driver: local
|
||||||
|
uploads:
|
||||||
|
driver: local
|
||||||
|
|
||||||
64
docker-compose.yml
Normal file
64
docker-compose.yml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# React Frontend
|
||||||
|
frontend:
|
||||||
|
build: ./frontend
|
||||||
|
ports:
|
||||||
|
- "5173:3000"
|
||||||
|
environment:
|
||||||
|
- REACT_APP_API_URL=http://147.45.146.17:8100
|
||||||
|
networks:
|
||||||
|
- erv-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Python FastAPI Backend
|
||||||
|
backend:
|
||||||
|
build: ./backend
|
||||||
|
ports:
|
||||||
|
- "8100:8100"
|
||||||
|
environment:
|
||||||
|
- REDIS_URL=redis://redis:6379
|
||||||
|
- POSTGRES_URL=postgresql://erv_user:erv_password@postgres:5432/erv_db
|
||||||
|
- RABBITMQ_URL=amqp://admin:tyejvtej@185.197.75.249:5672
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
- postgres
|
||||||
|
networks:
|
||||||
|
- erv-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Redis для кеширования
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
networks:
|
||||||
|
- erv-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# PostgreSQL для логов и аналитики
|
||||||
|
postgres:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=erv_db
|
||||||
|
- POSTGRES_USER=erv_user
|
||||||
|
- POSTGRES_PASSWORD=erv_password
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- erv-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
redis_data:
|
||||||
|
postgres_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
erv-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
26
frontend/Dockerfile
Normal file
26
frontend/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# React Frontend Dockerfile
|
||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
# Устанавливаем рабочую директорию
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копируем package.json
|
||||||
|
COPY package.json ./
|
||||||
|
|
||||||
|
# Устанавливаем зависимости
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Копируем исходный код
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Собираем приложение
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Устанавливаем serve для статических файлов
|
||||||
|
RUN npm install -g serve
|
||||||
|
|
||||||
|
# Открываем порт
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Запускаем приложение
|
||||||
|
CMD ["serve", "-s", "dist", "-l", "3000"]
|
||||||
15
frontend/index.html
Normal file
15
frontend/index.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>ERV Insurance Platform</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
@@ -9,7 +9,8 @@
|
|||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"type-check": "tsc --noEmit"
|
"type-check": "tsc --noEmit",
|
||||||
|
"start": "serve -s dist -l 3000"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
@@ -23,7 +24,8 @@
|
|||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"imask": "^7.6.1",
|
"imask": "^7.6.1",
|
||||||
"react-dropzone": "^14.3.5",
|
"react-dropzone": "^14.3.5",
|
||||||
"socket.io-client": "^4.8.1"
|
"socket.io-client": "^4.8.1",
|
||||||
|
"serve": "^14.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.3.11",
|
"@types/react": "^18.3.11",
|
||||||
|
|||||||
@@ -12,3 +12,4 @@
|
|||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -127,3 +127,4 @@
|
|||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,124 +1,12 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import ClaimForm from './pages/ClaimForm'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
|
||||||
interface APIInfo {
|
|
||||||
platform?: string;
|
|
||||||
version?: string;
|
|
||||||
features?: string[];
|
|
||||||
tech_stack?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [apiInfo, setApiInfo] = useState<APIInfo | null>(null)
|
|
||||||
const [health, setHealth] = useState<any>(null)
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Проверяем подключение к API
|
|
||||||
Promise.all([
|
|
||||||
fetch('http://147.45.146.17:8100/api/v1/info').then(r => r.json()),
|
|
||||||
fetch('http://147.45.146.17:8100/health').then(r => r.json())
|
|
||||||
])
|
|
||||||
.then(([info, healthData]) => {
|
|
||||||
setApiInfo(info)
|
|
||||||
setHealth(healthData)
|
|
||||||
setLoading(false)
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('API Error:', err)
|
|
||||||
setLoading(false)
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="App">
|
||||||
<header className="app-header">
|
<ClaimForm />
|
||||||
<h1>🚀 ERV Insurance Platform</h1>
|
|
||||||
<p>Python FastAPI + React TypeScript</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main className="app-main">
|
|
||||||
{loading ? (
|
|
||||||
<div className="loading">⏳ Подключение к API...</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="card">
|
|
||||||
<h2>📊 Информация о платформе</h2>
|
|
||||||
{apiInfo ? (
|
|
||||||
<>
|
|
||||||
<p><strong>Платформа:</strong> {apiInfo.platform}</p>
|
|
||||||
<p><strong>Версия:</strong> {apiInfo.version}</p>
|
|
||||||
|
|
||||||
<h3>✨ Возможности:</h3>
|
|
||||||
<ul>
|
|
||||||
{apiInfo.features?.map((f, i) => (
|
|
||||||
<li key={i}>{f}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3>🛠️ Технологический стек:</h3>
|
|
||||||
<pre>{JSON.stringify(apiInfo.tech_stack, null, 2)}</pre>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p className="error">❌ Не удалось получить данные</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="card">
|
|
||||||
<h2>🏥 Здоровье сервисов</h2>
|
|
||||||
{health ? (
|
|
||||||
<>
|
|
||||||
<p className={health.status === 'healthy' ? 'success' : 'warning'}>
|
|
||||||
Статус: <strong>{health.status}</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h3>Сервисы:</h3>
|
|
||||||
<ul className="services">
|
|
||||||
{Object.entries(health.services || {}).map(([name, status]) => (
|
|
||||||
<li key={name}>
|
|
||||||
<span className={status === 'ok' ? 'status-ok' : 'status-error'}>
|
|
||||||
{status === 'ok' ? '✅' : '❌'}
|
|
||||||
</span>
|
|
||||||
<strong>{name}:</strong> {String(status)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p className="error">❌ Health check недоступен</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="card">
|
|
||||||
<h2>🔗 Полезные ссылки</h2>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="http://147.45.146.17:8100/docs" target="_blank">
|
|
||||||
📚 API Документация (Swagger UI)
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="http://147.45.146.17:8100/health" target="_blank">
|
|
||||||
🏥 Health Check
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="http://147.45.146.17:3002" target="_blank">
|
|
||||||
🐙 Gitea (Git репозиторий)
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer className="app-footer">
|
|
||||||
<p>© 2025 ERV Insurance Platform | Powered by FastAPI + React</p>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
|
||||||
|
|||||||
199
frontend/src/components/form/Step1Phone.tsx
Normal file
199
frontend/src/components/form/Step1Phone.tsx
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Form, Input, Button, message, Space } from 'antd';
|
||||||
|
import { PhoneOutlined, SafetyOutlined, FileProtectOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
formData: any;
|
||||||
|
updateFormData: (data: any) => void;
|
||||||
|
onNext: () => void;
|
||||||
|
isPhoneVerified: boolean;
|
||||||
|
setIsPhoneVerified: (verified: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Step1Phone({ formData, updateFormData, onNext, isPhoneVerified, setIsPhoneVerified }: Props) {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [codeSent, setCodeSent] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [verifyLoading, setVerifyLoading] = useState(false);
|
||||||
|
|
||||||
|
const sendCode = async () => {
|
||||||
|
try {
|
||||||
|
const phone = form.getFieldValue('phone');
|
||||||
|
if (!phone) {
|
||||||
|
message.error('Введите номер телефона');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('http://147.45.146.17:8100/api/v1/sms/send', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ phone }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
message.success('Код отправлен на ваш телефон');
|
||||||
|
setCodeSent(true);
|
||||||
|
if (result.debug_code) {
|
||||||
|
message.info(`DEBUG: Код ${result.debug_code}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error(result.detail || 'Ошибка отправки кода');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('Ошибка соединения с сервером');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyCode = async () => {
|
||||||
|
try {
|
||||||
|
const phone = form.getFieldValue('phone');
|
||||||
|
const code = form.getFieldValue('smsCode');
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
message.error('Введите код из SMS');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVerifyLoading(true);
|
||||||
|
const response = await fetch('http://147.45.146.17:8100/api/v1/sms/verify', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ phone, code }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
message.success('Телефон подтвержден!');
|
||||||
|
setIsPhoneVerified(true);
|
||||||
|
} else {
|
||||||
|
message.error(result.detail || 'Неверный код');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('Ошибка соединения с сервером');
|
||||||
|
} finally {
|
||||||
|
setVerifyLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = async () => {
|
||||||
|
try {
|
||||||
|
const values = await form.validateFields();
|
||||||
|
updateFormData(values);
|
||||||
|
onNext();
|
||||||
|
} catch (error) {
|
||||||
|
message.error('Заполните все обязательные поля');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
initialValues={formData}
|
||||||
|
style={{ marginTop: 24 }}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label="Номер телефона"
|
||||||
|
name="phone"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Введите номер телефона' },
|
||||||
|
{ pattern: /^\+7\d{10}$/, message: 'Формат: +79001234567' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<PhoneOutlined />}
|
||||||
|
placeholder="+79001234567"
|
||||||
|
disabled={isPhoneVerified}
|
||||||
|
maxLength={12}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{!isPhoneVerified && (
|
||||||
|
<>
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={sendCode}
|
||||||
|
loading={loading}
|
||||||
|
disabled={codeSent}
|
||||||
|
>
|
||||||
|
{codeSent ? 'Код отправлен' : 'Отправить код'}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{codeSent && (
|
||||||
|
<Form.Item
|
||||||
|
label="Код из SMS"
|
||||||
|
name="smsCode"
|
||||||
|
rules={[{ required: true, message: 'Введите код' }, { len: 6, message: '6 цифр' }]}
|
||||||
|
>
|
||||||
|
<Space.Compact style={{ width: '100%' }}>
|
||||||
|
<Input
|
||||||
|
prefix={<SafetyOutlined />}
|
||||||
|
placeholder="123456"
|
||||||
|
maxLength={6}
|
||||||
|
style={{ width: '70%' }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={verifyCode}
|
||||||
|
loading={verifyLoading}
|
||||||
|
style={{ width: '30%' }}
|
||||||
|
>
|
||||||
|
Проверить
|
||||||
|
</Button>
|
||||||
|
</Space.Compact>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isPhoneVerified && (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
label="Email (необязательно)"
|
||||||
|
name="email"
|
||||||
|
rules={[{ type: 'email', message: 'Неверный формат email' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="example@mail.ru" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="ИНН (необязательно)"
|
||||||
|
name="inn"
|
||||||
|
>
|
||||||
|
<Input placeholder="1234567890" maxLength={12} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Номер полиса"
|
||||||
|
name="policyNumber"
|
||||||
|
rules={[{ required: true, message: 'Введите номер полиса' }]}
|
||||||
|
>
|
||||||
|
<Input prefix={<FileProtectOutlined />} placeholder="123456789" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Серия полиса (необязательно)"
|
||||||
|
name="policySeries"
|
||||||
|
>
|
||||||
|
<Input placeholder="AB" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" onClick={handleNext} size="large" block>
|
||||||
|
Далее
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
122
frontend/src/components/form/Step2Details.tsx
Normal file
122
frontend/src/components/form/Step2Details.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { Form, Input, DatePicker, Select, Button, Upload, message } from 'antd';
|
||||||
|
import { UploadOutlined } from '@ant-design/icons';
|
||||||
|
import type { UploadFile } from 'antd/es/upload/interface';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
const { TextArea } = Input;
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
formData: any;
|
||||||
|
updateFormData: (data: any) => void;
|
||||||
|
onNext: () => void;
|
||||||
|
onPrev: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Step2Details({ formData, updateFormData, onNext, onPrev }: Props) {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||||
|
|
||||||
|
const handleNext = async () => {
|
||||||
|
try {
|
||||||
|
const values = await form.validateFields();
|
||||||
|
updateFormData({
|
||||||
|
...values,
|
||||||
|
incidentDate: values.incidentDate?.format('YYYY-MM-DD'),
|
||||||
|
uploadedFiles: fileList.map(f => f.uid),
|
||||||
|
});
|
||||||
|
onNext();
|
||||||
|
} catch (error) {
|
||||||
|
message.error('Заполните все обязательные поля');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadProps = {
|
||||||
|
fileList,
|
||||||
|
beforeUpload: (file: File) => {
|
||||||
|
const isImage = file.type.startsWith('image/');
|
||||||
|
const isPDF = file.type === 'application/pdf';
|
||||||
|
if (!isImage && !isPDF) {
|
||||||
|
message.error('Можно загружать только изображения и PDF');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const isLt10M = file.size / 1024 / 1024 < 10;
|
||||||
|
if (!isLt10M) {
|
||||||
|
message.error('Файл должен быть меньше 10MB');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFileList([...fileList, {
|
||||||
|
uid: Math.random().toString(),
|
||||||
|
name: file.name,
|
||||||
|
status: 'done',
|
||||||
|
url: URL.createObjectURL(file),
|
||||||
|
} as UploadFile]);
|
||||||
|
|
||||||
|
return false; // Отключаем автозагрузку
|
||||||
|
},
|
||||||
|
onRemove: (file: UploadFile) => {
|
||||||
|
setFileList(fileList.filter(f => f.uid !== file.uid));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
initialValues={formData}
|
||||||
|
style={{ marginTop: 24 }}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label="Дата происшествия"
|
||||||
|
name="incidentDate"
|
||||||
|
>
|
||||||
|
<DatePicker style={{ width: '100%' }} format="DD.MM.YYYY" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Тип транспорта"
|
||||||
|
name="transportType"
|
||||||
|
>
|
||||||
|
<Select placeholder="Выберите тип транспорта">
|
||||||
|
<Option value="air">Авиа</Option>
|
||||||
|
<Option value="train">Поезд</Option>
|
||||||
|
<Option value="bus">Автобус</Option>
|
||||||
|
<Option value="ship">Водный транспорт</Option>
|
||||||
|
<Option value="other">Другое</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Описание происшествия"
|
||||||
|
name="incidentDescription"
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
rows={4}
|
||||||
|
placeholder="Опишите что произошло..."
|
||||||
|
maxLength={1000}
|
||||||
|
showCount
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="Документы (билеты, справки, чеки)">
|
||||||
|
<Upload {...uploadProps} listType="picture">
|
||||||
|
<Button icon={<UploadOutlined />}>Загрузить файлы</Button>
|
||||||
|
</Upload>
|
||||||
|
<div style={{ marginTop: 8, color: '#999', fontSize: 12 }}>
|
||||||
|
Максимум 10 MB на файл. Форматы: JPG, PNG, PDF, HEIC
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
|
<Button onClick={onPrev}>Назад</Button>
|
||||||
|
<Button type="primary" onClick={handleNext} style={{ flex: 1 }}>
|
||||||
|
Далее
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
131
frontend/src/components/form/Step3Payment.tsx
Normal file
131
frontend/src/components/form/Step3Payment.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { Form, Input, Radio, Button, Select, message } from 'antd';
|
||||||
|
import { BankOutlined, CreditCardOutlined, QrcodeOutlined } from '@ant-design/icons';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
formData: any;
|
||||||
|
updateFormData: (data: any) => void;
|
||||||
|
onPrev: () => void;
|
||||||
|
onSubmit: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Step3Payment({ formData, updateFormData, onPrev, onSubmit }: Props) {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [paymentMethod, setPaymentMethod] = useState(formData.paymentMethod || 'sbp');
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
const values = await form.validateFields();
|
||||||
|
updateFormData(values);
|
||||||
|
|
||||||
|
setSubmitting(true);
|
||||||
|
await onSubmit();
|
||||||
|
} catch (error) {
|
||||||
|
message.error('Заполните все обязательные поля');
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
initialValues={formData}
|
||||||
|
style={{ marginTop: 24 }}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label="Способ выплаты"
|
||||||
|
name="paymentMethod"
|
||||||
|
rules={[{ required: true, message: 'Выберите способ выплаты' }]}
|
||||||
|
>
|
||||||
|
<Radio.Group onChange={(e) => setPaymentMethod(e.target.value)}>
|
||||||
|
<Radio.Button value="sbp">
|
||||||
|
<QrcodeOutlined /> СБП (Быстрые платежи)
|
||||||
|
</Radio.Button>
|
||||||
|
<Radio.Button value="card">
|
||||||
|
<CreditCardOutlined /> Карта
|
||||||
|
</Radio.Button>
|
||||||
|
<Radio.Button value="bank_transfer">
|
||||||
|
<BankOutlined /> Банковский счет
|
||||||
|
</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{paymentMethod === 'sbp' && (
|
||||||
|
<Form.Item
|
||||||
|
label="Банк для СБП"
|
||||||
|
name="bankName"
|
||||||
|
rules={[{ required: true, message: 'Выберите банк' }]}
|
||||||
|
>
|
||||||
|
<Select placeholder="Выберите ваш банк">
|
||||||
|
<Option value="sberbank">Сбербанк</Option>
|
||||||
|
<Option value="tinkoff">Тинькофф</Option>
|
||||||
|
<Option value="vtb">ВТБ</Option>
|
||||||
|
<Option value="alfabank">Альфа-Банк</Option>
|
||||||
|
<Option value="raiffeisen">Райффайзенбанк</Option>
|
||||||
|
<Option value="other">Другой</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{paymentMethod === 'card' && (
|
||||||
|
<Form.Item
|
||||||
|
label="Номер карты"
|
||||||
|
name="cardNumber"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Введите номер карты' },
|
||||||
|
{ pattern: /^\d{16}$/, message: '16 цифр без пробелов' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<CreditCardOutlined />}
|
||||||
|
placeholder="1234567890123456"
|
||||||
|
maxLength={16}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{paymentMethod === 'bank_transfer' && (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
label="Название банка"
|
||||||
|
name="bankName"
|
||||||
|
rules={[{ required: true, message: 'Введите название банка' }]}
|
||||||
|
>
|
||||||
|
<Input prefix={<BankOutlined />} placeholder="Сбербанк" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="Номер счета"
|
||||||
|
name="accountNumber"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Введите номер счета' },
|
||||||
|
{ pattern: /^\d{20}$/, message: '20 цифр' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="12345678901234567890" maxLength={20} />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<div style={{ display: 'flex', gap: 8, marginTop: 32 }}>
|
||||||
|
<Button onClick={onPrev}>Назад</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
loading={submitting}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
Отправить заявку
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -15,3 +15,4 @@ body {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
51
frontend/src/pages/ClaimForm.css
Normal file
51
frontend/src/pages/ClaimForm.css
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
.claim-form-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 40px 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.claim-form-card {
|
||||||
|
max-width: 800px;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.claim-form-card .ant-card-head {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.claim-form-card .ant-card-head-title {
|
||||||
|
color: white;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-content {
|
||||||
|
min-height: 400px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.claim-form-container {
|
||||||
|
padding: 20px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.claim-form-card {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-content {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
147
frontend/src/pages/ClaimForm.tsx
Normal file
147
frontend/src/pages/ClaimForm.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Steps, Card, message } from 'antd';
|
||||||
|
import Step1Phone from '../components/form/Step1Phone';
|
||||||
|
import Step2Details from '../components/form/Step2Details';
|
||||||
|
import Step3Payment from '../components/form/Step3Payment';
|
||||||
|
import './ClaimForm.css';
|
||||||
|
|
||||||
|
const { Step } = Steps;
|
||||||
|
|
||||||
|
interface FormData {
|
||||||
|
// Шаг 1
|
||||||
|
phone: string;
|
||||||
|
email?: string;
|
||||||
|
inn?: string;
|
||||||
|
policyNumber: string;
|
||||||
|
policySeries?: string;
|
||||||
|
|
||||||
|
// Шаг 2
|
||||||
|
incidentDate?: string;
|
||||||
|
incidentDescription?: string;
|
||||||
|
transportType?: string;
|
||||||
|
uploadedFiles?: string[];
|
||||||
|
|
||||||
|
// Шаг 3
|
||||||
|
paymentMethod: string;
|
||||||
|
bankName?: string;
|
||||||
|
cardNumber?: string;
|
||||||
|
accountNumber?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ClaimForm() {
|
||||||
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
|
const [formData, setFormData] = useState<FormData>({
|
||||||
|
phone: '',
|
||||||
|
policyNumber: '',
|
||||||
|
paymentMethod: 'sbp',
|
||||||
|
});
|
||||||
|
const [isPhoneVerified, setIsPhoneVerified] = useState(false);
|
||||||
|
|
||||||
|
const updateFormData = (data: Partial<FormData>) => {
|
||||||
|
setFormData({ ...formData, ...data });
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextStep = () => {
|
||||||
|
setCurrentStep(currentStep + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevStep = () => {
|
||||||
|
setCurrentStep(currentStep - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://147.45.146.17:8100/api/v1/claims/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
phone: formData.phone,
|
||||||
|
email: formData.email,
|
||||||
|
inn: formData.inn,
|
||||||
|
policy_number: formData.policyNumber,
|
||||||
|
policy_series: formData.policySeries,
|
||||||
|
incident_date: formData.incidentDate,
|
||||||
|
incident_description: formData.incidentDescription,
|
||||||
|
transport_type: formData.transportType,
|
||||||
|
payment_method: formData.paymentMethod,
|
||||||
|
bank_name: formData.bankName,
|
||||||
|
card_number: formData.cardNumber,
|
||||||
|
account_number: formData.accountNumber,
|
||||||
|
uploaded_files: formData.uploadedFiles || [],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
message.success(`Заявка ${result.claim_number} успешно создана!`);
|
||||||
|
// Сброс формы
|
||||||
|
setFormData({
|
||||||
|
phone: '',
|
||||||
|
policyNumber: '',
|
||||||
|
paymentMethod: 'sbp',
|
||||||
|
});
|
||||||
|
setCurrentStep(0);
|
||||||
|
setIsPhoneVerified(false);
|
||||||
|
} else {
|
||||||
|
message.error('Ошибка при создании заявки');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('Ошибка соединения с сервером');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
title: 'Телефон и полис',
|
||||||
|
content: (
|
||||||
|
<Step1Phone
|
||||||
|
formData={formData}
|
||||||
|
updateFormData={updateFormData}
|
||||||
|
onNext={nextStep}
|
||||||
|
isPhoneVerified={isPhoneVerified}
|
||||||
|
setIsPhoneVerified={setIsPhoneVerified}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Детали происшествия',
|
||||||
|
content: (
|
||||||
|
<Step2Details
|
||||||
|
formData={formData}
|
||||||
|
updateFormData={updateFormData}
|
||||||
|
onNext={nextStep}
|
||||||
|
onPrev={prevStep}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Способ выплаты',
|
||||||
|
content: (
|
||||||
|
<Step3Payment
|
||||||
|
formData={formData}
|
||||||
|
updateFormData={updateFormData}
|
||||||
|
onPrev={prevStep}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="claim-form-container">
|
||||||
|
<Card title="Подать заявку на выплату" className="claim-form-card">
|
||||||
|
<Steps current={currentStep} className="steps">
|
||||||
|
{steps.map((item) => (
|
||||||
|
<Step key={item.title} title={item.title} />
|
||||||
|
))}
|
||||||
|
</Steps>
|
||||||
|
<div className="steps-content">{steps[currentStep].content}</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -20,3 +20,4 @@
|
|||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
12
frontend/tsconfig.node.json
Normal file
12
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -15,3 +15,4 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
164
index.html
Normal file
164
index.html
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ERV Insurance Platform - MVP</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container { max-width: 1200px; margin: 0 auto; }
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
font-size: 3em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 30px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
.status.success { background: #10b981; color: white; }
|
||||||
|
.status.warning { background: #f59e0b; color: white; }
|
||||||
|
.status.error { background: #ef4444; color: white; }
|
||||||
|
.loading { text-align: center; padding: 40px; color: #666; }
|
||||||
|
.links { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.btn:hover { background: #5568d3; transform: translateY(-2px); }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }
|
||||||
|
pre {
|
||||||
|
background: #f3f4f6;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🚀 ERV Insurance Platform</h1>
|
||||||
|
<p style="font-size: 1.2em;">Python FastAPI + React TypeScript - MVP</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>📊 Статус системы</h2>
|
||||||
|
<div id="status-container" class="loading">⏳ Проверяю сервисы...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card">
|
||||||
|
<h3>🔗 API Endpoints</h3>
|
||||||
|
<div class="links">
|
||||||
|
<a href="http://147.45.146.17:8100/docs" target="_blank" class="btn">📚 Swagger UI</a>
|
||||||
|
<a href="http://147.45.146.17:8100/health" target="_blank" class="btn">🏥 Health Check</a>
|
||||||
|
<a href="http://147.45.146.17:8100/api/v1/info" target="_blank" class="btn">ℹ️ Info</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>🛠️ Технологии</h3>
|
||||||
|
<div id="tech-stack"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>✨ Возможности</h3>
|
||||||
|
<div id="features"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>📋 Детальная информация API</h3>
|
||||||
|
<pre id="api-details">Загрузка...</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function checkAPI() {
|
||||||
|
const statusContainer = document.getElementById('status-container');
|
||||||
|
const techStack = document.getElementById('tech-stack');
|
||||||
|
const features = document.getElementById('features');
|
||||||
|
const apiDetails = document.getElementById('api-details');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Проверяем API
|
||||||
|
const [healthRes, infoRes, testRes] = await Promise.all([
|
||||||
|
fetch('http://147.45.146.17:8100/health'),
|
||||||
|
fetch('http://147.45.146.17:8100/api/v1/info'),
|
||||||
|
fetch('http://147.45.146.17:8100/api/v1/test')
|
||||||
|
]);
|
||||||
|
|
||||||
|
const health = await healthRes.json();
|
||||||
|
const info = await infoRes.json();
|
||||||
|
const test = await testRes.json();
|
||||||
|
|
||||||
|
// Статус
|
||||||
|
statusContainer.innerHTML = `
|
||||||
|
<div style="margin: 20px 0;">
|
||||||
|
<span class="status success">✅ Backend API работает!</span>
|
||||||
|
<span class="status success">✅ Health: ${health.status}</span>
|
||||||
|
<span class="status success">✅ Version: ${info.version}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Технологии
|
||||||
|
const stack = info.tech_stack || {};
|
||||||
|
techStack.innerHTML = Object.entries(stack)
|
||||||
|
.map(([key, value]) => `<div><strong>${key}:</strong> ${value}</div>`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
// Возможности
|
||||||
|
features.innerHTML = (info.features || [])
|
||||||
|
.map(f => `<div>✓ ${f}</div>`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
// Детали
|
||||||
|
apiDetails.textContent = JSON.stringify({ health, info, test }, null, 2);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
statusContainer.innerHTML = `
|
||||||
|
<span class="status error">❌ Ошибка подключения к API</span>
|
||||||
|
<p style="margin-top: 10px; color: #666;">
|
||||||
|
Проверьте что FastAPI запущен на порту 8100
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
apiDetails.textContent = `Ошибка: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запускаем проверку
|
||||||
|
checkAPI();
|
||||||
|
|
||||||
|
// Обновляем каждые 10 секунд
|
||||||
|
setInterval(checkAPI, 10000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
118
links.html
Normal file
118
links.html
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ERV Platform - Ссылки</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 50px auto;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.link-box {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.link-box h2 {
|
||||||
|
color: #667eea;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.link-box a {
|
||||||
|
display: block;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.link-box a:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
.description {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>🚀 ERV Insurance Platform - Ссылки для доступа</h1>
|
||||||
|
|
||||||
|
<div class="link-box">
|
||||||
|
<h2>🎨 Frontend (React приложение)</h2>
|
||||||
|
<a href="http://147.45.146.17:5173/" target="_blank">
|
||||||
|
http://147.45.146.17:5173/
|
||||||
|
</a>
|
||||||
|
<div class="description">
|
||||||
|
Красивая страница с информацией о платформе, статусом сервисов
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="link-box">
|
||||||
|
<h2>🔌 Backend API</h2>
|
||||||
|
<a href="http://147.45.146.17:8100/" target="_blank">
|
||||||
|
http://147.45.146.17:8100/
|
||||||
|
</a>
|
||||||
|
<div class="description">
|
||||||
|
JSON API endpoint (главная страница)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="link-box">
|
||||||
|
<h2>📚 API Документация (Swagger UI)</h2>
|
||||||
|
<a href="http://147.45.146.17:8100/docs" target="_blank">
|
||||||
|
http://147.45.146.17:8100/docs
|
||||||
|
</a>
|
||||||
|
<div class="description">
|
||||||
|
Интерактивная документация API - можно тестировать прямо в браузере!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="link-box">
|
||||||
|
<h2>🏥 Health Check</h2>
|
||||||
|
<a href="http://147.45.146.17:8100/health" target="_blank">
|
||||||
|
http://147.45.146.17:8100/health
|
||||||
|
</a>
|
||||||
|
<div class="description">
|
||||||
|
Проверка работоспособности всех сервисов (Redis, PostgreSQL, OCR)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="link-box">
|
||||||
|
<h2>🧪 Test Endpoint</h2>
|
||||||
|
<a href="http://147.45.146.17:8100/api/v1/test" target="_blank">
|
||||||
|
http://147.45.146.17:8100/api/v1/test
|
||||||
|
</a>
|
||||||
|
<div class="description">
|
||||||
|
Тестовый endpoint с информацией о подключенных сервисах
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="link-box">
|
||||||
|
<h2>🐙 Gitea (Git репозиторий)</h2>
|
||||||
|
<a href="http://147.45.146.17:3002/negodiy/erv-platform" target="_blank">
|
||||||
|
http://147.45.146.17:3002/negodiy/erv-platform
|
||||||
|
</a>
|
||||||
|
<div class="description">
|
||||||
|
Git репозиторий с кодом (логин: negodiy)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<p><strong>⚠️ Важно:</strong> Сначала нужно запустить оба сервера!</p>
|
||||||
|
<p>См. файл <code>START_HERE.md</code> с инструкциями</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
6
start_backend.sh
Executable file
6
start_backend.sh
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd /var/www/fastuser/data/www/crm.clientright.ru/erv_platform/backend
|
||||||
|
source venv/bin/activate
|
||||||
|
uvicorn app.main:app --host 0.0.0.0 --port 8100 --reload
|
||||||
|
|
||||||
|
|
||||||
5
start_frontend.sh
Executable file
5
start_frontend.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd /var/www/fastuser/data/www/crm.clientright.ru/erv_platform/frontend
|
||||||
|
npm run dev -- --host 0.0.0.0
|
||||||
|
|
||||||
|
|
||||||
96
ЗАПУСК.md
Normal file
96
ЗАПУСК.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# ⚡ ПРОСТОЙ ЗАПУСК MVP
|
||||||
|
|
||||||
|
## 🎯 Фёдор, делай так:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## СПОСОБ 1: Через скрипты (ПРОЩЕ!)
|
||||||
|
|
||||||
|
### Терминал 1 - Backend:
|
||||||
|
```bash
|
||||||
|
cd /var/www/fastuser/data/www/crm.clientright.ru/erv_platform
|
||||||
|
./start_backend.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Терминал 2 - Frontend:
|
||||||
|
```bash
|
||||||
|
cd /var/www/fastuser/data/www/crm.clientright.ru/erv_platform
|
||||||
|
./start_frontend.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## СПОСОБ 2: Вручную (если скрипты не работают)
|
||||||
|
|
||||||
|
### Терминал 1 - Backend:
|
||||||
|
```bash
|
||||||
|
cd /var/www/fastuser/data/www/crm.clientright.ru/erv_platform/backend
|
||||||
|
source venv/bin/activate
|
||||||
|
uvicorn app.main:app --host 0.0.0.0 --port 8100 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
### Терминал 2 - Frontend:
|
||||||
|
```bash
|
||||||
|
cd /var/www/fastuser/data/www/crm.clientright.ru/erv_platform/frontend
|
||||||
|
npm install
|
||||||
|
npm run dev -- --host 0.0.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 ССЫЛКИ (открывай после запуска):
|
||||||
|
|
||||||
|
### ✅ Frontend (красивая страница):
|
||||||
|
http://147.45.146.17:5173/
|
||||||
|
|
||||||
|
### ✅ API Документация (Swagger UI):
|
||||||
|
http://147.45.146.17:8100/docs
|
||||||
|
|
||||||
|
### ✅ Health Check (проверка сервисов):
|
||||||
|
http://147.45.146.17:8100/health
|
||||||
|
|
||||||
|
### ✅ Test API:
|
||||||
|
http://147.45.146.17:8100/api/v1/test
|
||||||
|
|
||||||
|
### ✅ Git репозиторий:
|
||||||
|
http://147.45.146.17:3002/negodiy/erv-platform
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Что увидишь на http://147.45.146.17:5173/
|
||||||
|
|
||||||
|
Красивую страницу с:
|
||||||
|
- 📊 Информация о платформе
|
||||||
|
- 🏥 Статус всех сервисов (Redis ✅, PostgreSQL ✅, OCR ✅)
|
||||||
|
- ✨ Список возможностей
|
||||||
|
- 🛠️ Технологический стек
|
||||||
|
- 🔗 Ссылки на API docs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Проблемы?
|
||||||
|
|
||||||
|
### Backend не запустился?
|
||||||
|
|
||||||
|
Смотри логи:
|
||||||
|
```bash
|
||||||
|
tail -f /var/www/fastuser/data/www/crm.clientright.ru/erv_platform/backend/uvicorn.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend не запустился?
|
||||||
|
|
||||||
|
Проверь Node.js:
|
||||||
|
```bash
|
||||||
|
node --version # Должно быть >= 18
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Скажи мне:
|
||||||
|
|
||||||
|
1. ✅ "Запустилось!" - и я продолжу делать полную функциональность
|
||||||
|
2. ❌ "Ошибка: ..." - покажи текст ошибки и я исправлю
|
||||||
|
|
||||||
|
**ЗАПУСКАЙ!** 🚀
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user