🚀 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
|
||||
*.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).
|
||||
|
||||
**Начинаю прямо сейчас!** 🚀
|
||||
|
||||
|
||||
@@ -181,3 +181,4 @@ git push -u origin main
|
||||
|
||||
**Удачи!** 🚀
|
||||
|
||||
|
||||
|
||||
@@ -170,3 +170,4 @@ git push origin main
|
||||
**Автор**: AI Assistant + Фёдор
|
||||
**Дата**: 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 functools import lru_cache
|
||||
from typing import List
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# App
|
||||
# ============================================
|
||||
# APPLICATION
|
||||
# ============================================
|
||||
app_name: str = "ERV Insurance Platform"
|
||||
app_env: str = "development"
|
||||
debug: bool = True
|
||||
@@ -16,47 +19,144 @@ class Settings(BaseSettings):
|
||||
backend_url: str = "http://localhost:8100"
|
||||
frontend_url: str = "http://localhost:5173"
|
||||
|
||||
# PostgreSQL
|
||||
# ============================================
|
||||
# DATABASE (PostgreSQL)
|
||||
# ============================================
|
||||
postgres_host: str = "147.45.189.234"
|
||||
postgres_port: int = 5432
|
||||
postgres_db: str = "default_db"
|
||||
postgres_user: str = "gen_user"
|
||||
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_port: int = 6379
|
||||
redis_password: str = "CRM_Redis_Pass_2025_Secure!"
|
||||
redis_db: int = 0
|
||||
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_port: int = 5672
|
||||
rabbitmq_user: str = "admin"
|
||||
rabbitmq_password: str = "tyejvtej"
|
||||
rabbitmq_vhost: str = "/"
|
||||
|
||||
# OCR Service
|
||||
ocr_api_url: str = "http://147.45.146.17:8001"
|
||||
@property
|
||||
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_base_url: str = "https://openrouter.ai/api/v1"
|
||||
openrouter_model: str = "google/gemini-2.0-flash-001"
|
||||
|
||||
# ============================================
|
||||
# FLIGHT APIs
|
||||
# ============================================
|
||||
# FlightAware
|
||||
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_origins: list = [
|
||||
"http://localhost:5173",
|
||||
"http://147.45.146.17:5173",
|
||||
"https://erv-claims.clientright.ru"
|
||||
]
|
||||
# ============================================
|
||||
cors_origins: str = "http://localhost:5173,http://147.45.146.17:5173,https://erv-claims.clientright.ru,http://crm.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:
|
||||
env_file = "../.env"
|
||||
case_sensitive = False
|
||||
extra = "ignore" # Игнорируем лишние поля из .env
|
||||
|
||||
|
||||
@lru_cache()
|
||||
@@ -66,3 +166,4 @@ def get_settings() -> Settings:
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
|
||||
@@ -3,33 +3,84 @@ ERV Insurance Platform - FastAPI Backend
|
||||
"""
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from app.config import settings
|
||||
import redis
|
||||
import asyncpg
|
||||
from contextlib import asynccontextmanager
|
||||
import logging
|
||||
|
||||
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 приложение
|
||||
app = FastAPI(
|
||||
title="ERV Insurance Platform API",
|
||||
description="API для обработки страховых обращений с OCR, AI и интеграциями",
|
||||
description="API для обработки страховых обращений",
|
||||
version="1.0.0",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc"
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# CORS middleware
|
||||
# CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.cors_origins,
|
||||
allow_origins=settings.cors_origins_list,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# API Routes
|
||||
app.include_router(sms.router)
|
||||
app.include_router(claims.router)
|
||||
|
||||
# ============================================
|
||||
# HEALTH CHECKS
|
||||
# ============================================
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
@@ -37,140 +88,111 @@ async def root():
|
||||
return {
|
||||
"message": "🚀 ERV Insurance Platform API",
|
||||
"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")
|
||||
async def health_check():
|
||||
"""Проверка здоровья сервисов"""
|
||||
async def health():
|
||||
"""Health check - проверка всех сервисов"""
|
||||
health_status = {
|
||||
"api": "ok",
|
||||
"redis": "checking",
|
||||
"postgres": "checking",
|
||||
"ocr": "checking"
|
||||
"status": "ok",
|
||||
"message": "API работает!",
|
||||
"services": {}
|
||||
}
|
||||
|
||||
# Проверка 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
|
||||
try:
|
||||
conn = await asyncpg.connect(
|
||||
host=settings.postgres_host,
|
||||
port=settings.postgres_port,
|
||||
database=settings.postgres_db,
|
||||
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
|
||||
pg_healthy = await db.health_check()
|
||||
health_status["services"]["postgresql"] = {
|
||||
"status": "✅ healthy" if pg_healthy else "❌ unhealthy",
|
||||
"connected": pg_healthy
|
||||
}
|
||||
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")
|
||||
async def test_endpoint():
|
||||
async def test():
|
||||
"""Тестовый endpoint"""
|
||||
return {
|
||||
"message": "✅ API работает!",
|
||||
"env": settings.app_env,
|
||||
"debug": settings.debug,
|
||||
"success": True,
|
||||
"message": "✅ Backend API работает!",
|
||||
"services": {
|
||||
"redis": f"{settings.redis_host}:{settings.redis_port}",
|
||||
"postgres": f"{settings.postgres_host}:{settings.postgres_port}",
|
||||
"rabbitmq": f"{settings.rabbitmq_host}:{settings.rabbitmq_port}",
|
||||
"ocr": settings.ocr_api_url
|
||||
"redis": "localhost:6379",
|
||||
"postgres": "147.45.189.234:5432",
|
||||
"ocr": "147.45.146.17:8001",
|
||||
"rabbitmq": "185.197.75.249:5672"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/v1/info")
|
||||
async def get_info():
|
||||
async def info():
|
||||
"""Информация о платформе"""
|
||||
return {
|
||||
"platform": "ERV Insurance Claims",
|
||||
"version": "1.0.0",
|
||||
"features": [
|
||||
"OCR документов (паспорт, билеты)",
|
||||
"AI автозаполнение (Gemini Vision)",
|
||||
"Проверка рейсов (FlightAware)",
|
||||
"СБП выплаты",
|
||||
"Интеграция с CRM"
|
||||
],
|
||||
"tech_stack": {
|
||||
"backend": "Python FastAPI",
|
||||
"frontend": "React TypeScript",
|
||||
"database": "PostgreSQL + MySQL",
|
||||
"cache": "Redis",
|
||||
"queue": "RabbitMQ",
|
||||
"storage": "S3 Timeweb",
|
||||
"ocr": "Internal Service",
|
||||
"ai": "OpenRouter Gemini 2.0"
|
||||
}
|
||||
"storage": "S3 Timeweb"
|
||||
},
|
||||
"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__":
|
||||
import uvicorn
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host="0.0.0.0",
|
||||
port=8100,
|
||||
reload=True
|
||||
)
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8100)
|
||||
|
||||
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",
|
||||
"preview": "vite preview",
|
||||
"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": {
|
||||
"react": "^18.3.1",
|
||||
@@ -23,7 +24,8 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"imask": "^7.6.1",
|
||||
"react-dropzone": "^14.3.5",
|
||||
"socket.io-client": "^4.8.1"
|
||||
"socket.io-client": "^4.8.1",
|
||||
"serve": "^14.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.11",
|
||||
|
||||
@@ -12,3 +12,4 @@
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
@@ -127,3 +127,4 @@
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,124 +1,12 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import ClaimForm from './pages/ClaimForm'
|
||||
import './App.css'
|
||||
|
||||
interface APIInfo {
|
||||
platform?: string;
|
||||
version?: string;
|
||||
features?: string[];
|
||||
tech_stack?: any;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="app">
|
||||
<header className="app-header">
|
||||
<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 className="App">
|
||||
<ClaimForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -9,3 +9,4 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
</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" }]
|
||||
}
|
||||
|
||||
|
||||
|
||||
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