fix: Перезапуск платформы - исправлены зависимости и TypeScript ошибки
- Исправлены TypeScript ошибки в Step3Payment.tsx (типизация, неиспользуемые импорты) - Добавлены недостающие зависимости: aiomysql, pymysql, python-multipart - Обновлен requirements.txt с актуальными версиями - Добавлены новые API endpoints: policy check, file upload - Добавлен policy_service для работы с MySQL - Все сервисы успешно запущены и работают - Обновлен SESSION_LOG с документацией процесса
This commit is contained in:
45
backend/app/api/policy.py
Normal file
45
backend/app/api/policy.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Policy API Routes - Проверка полисов
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from ..services.policy_service import policy_service
|
||||
|
||||
router = APIRouter(prefix="/api/v1/policy", tags=["Policy"])
|
||||
|
||||
|
||||
class PolicyCheckRequest(BaseModel):
|
||||
"""Запрос на проверку полиса"""
|
||||
voucher: str
|
||||
inn: str | None = None
|
||||
|
||||
|
||||
@router.post("/check")
|
||||
async def check_policy(request: PolicyCheckRequest):
|
||||
"""
|
||||
Проверить полис в БД
|
||||
|
||||
- **voucher**: Номер полиса
|
||||
- **inn**: ИНН (опционально)
|
||||
|
||||
Returns:
|
||||
- found: true/false
|
||||
- policy_data: данные полиса если найден
|
||||
"""
|
||||
policy = await policy_service.check_policy(request.voucher, request.inn)
|
||||
|
||||
if policy:
|
||||
return {
|
||||
"success": True,
|
||||
"found": True,
|
||||
"message": "Полис найден в базе",
|
||||
"policy_data": policy
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": True,
|
||||
"found": False,
|
||||
"message": "Полис не найден. Загрузите скан полиса.",
|
||||
"policy_data": None
|
||||
}
|
||||
|
||||
154
backend/app/api/upload.py
Normal file
154
backend/app/api/upload.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""
|
||||
Upload API Routes - Загрузка файлов с OCR
|
||||
"""
|
||||
from fastapi import APIRouter, UploadFile, File, HTTPException
|
||||
from typing import List
|
||||
import httpx
|
||||
import uuid
|
||||
import os
|
||||
from ..config import settings
|
||||
import logging
|
||||
|
||||
router = APIRouter(prefix="/api/v1/upload", tags=["Upload"])
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
UPLOAD_DIR = "/tmp/erv_uploads"
|
||||
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
||||
|
||||
|
||||
@router.post("/policy")
|
||||
async def upload_policy(file: UploadFile = File(...)):
|
||||
"""
|
||||
Загрузить скан полиса + OCR обработка
|
||||
|
||||
Returns:
|
||||
- file_id: ID загруженного файла
|
||||
- ocr_text: распознанный текст
|
||||
- extracted_data: извлеченные данные (номер полиса, серия, даты)
|
||||
"""
|
||||
try:
|
||||
# Генерируем уникальный ID
|
||||
file_id = str(uuid.uuid4())
|
||||
file_ext = file.filename.split('.')[-1] if '.' in file.filename else 'jpg'
|
||||
file_path = f"{UPLOAD_DIR}/{file_id}.{file_ext}"
|
||||
|
||||
# Сохраняем файл
|
||||
with open(file_path, "wb") as f:
|
||||
content = await file.read()
|
||||
f.write(content)
|
||||
|
||||
logger.info(f"📄 File saved: {file_path}")
|
||||
|
||||
# Отправляем на OCR
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||
with open(file_path, "rb") as f:
|
||||
files = {"file": (file.filename, f, file.content_type)}
|
||||
response = await client.post(
|
||||
f"{settings.ocr_api_url}/process",
|
||||
files=files
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
ocr_result = response.json()
|
||||
logger.info(f"✅ OCR completed for policy")
|
||||
|
||||
# TODO: Извлечь номер полиса, серию, даты из OCR текста
|
||||
# Используем regex или AI для парсинга
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"file_id": file_id,
|
||||
"ocr_text": ocr_result.get("text", ""),
|
||||
"extracted_data": {
|
||||
"policy_number": None, # TODO: парсинг
|
||||
"policy_series": None,
|
||||
"start_date": None,
|
||||
"end_date": None
|
||||
}
|
||||
}
|
||||
else:
|
||||
logger.error(f"OCR error: {response.status_code}")
|
||||
raise HTTPException(status_code=500, detail="OCR service error")
|
||||
|
||||
except Exception as ocr_error:
|
||||
logger.error(f"OCR processing error: {ocr_error}")
|
||||
# Возвращаем без OCR
|
||||
return {
|
||||
"success": True,
|
||||
"file_id": file_id,
|
||||
"ocr_text": "",
|
||||
"extracted_data": {},
|
||||
"message": "Файл загружен, но OCR не удалось выполнить"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"File upload error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/passport")
|
||||
async def upload_passport(file: UploadFile = File(...)):
|
||||
"""
|
||||
Загрузить скан паспорта + OCR для ФИО
|
||||
|
||||
Returns:
|
||||
- file_id: ID загруженного файла
|
||||
- ocr_text: распознанный текст
|
||||
- extracted_data: ФИО, дата рождения, серия/номер
|
||||
"""
|
||||
try:
|
||||
file_id = str(uuid.uuid4())
|
||||
file_ext = file.filename.split('.')[-1] if '.' in file.filename else 'jpg'
|
||||
file_path = f"{UPLOAD_DIR}/{file_id}.{file_ext}"
|
||||
|
||||
with open(file_path, "wb") as f:
|
||||
content = await file.read()
|
||||
f.write(content)
|
||||
|
||||
logger.info(f"📄 Passport saved: {file_path}")
|
||||
|
||||
# OCR обработка
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||
with open(file_path, "rb") as f:
|
||||
files = {"file": (file.filename, f, file.content_type)}
|
||||
response = await client.post(
|
||||
f"{settings.ocr_api_url}/process",
|
||||
files=files
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
ocr_result = response.json()
|
||||
logger.info(f"✅ OCR completed for passport")
|
||||
|
||||
# TODO: Извлечь ФИО через regex или AI
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"file_id": file_id,
|
||||
"ocr_text": ocr_result.get("text", ""),
|
||||
"extracted_data": {
|
||||
"full_name": None, # TODO: парсинг
|
||||
"birth_date": None,
|
||||
"passport_series": None,
|
||||
"passport_number": None
|
||||
}
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="OCR service error")
|
||||
|
||||
except Exception as ocr_error:
|
||||
logger.error(f"OCR error: {ocr_error}")
|
||||
return {
|
||||
"success": True,
|
||||
"file_id": file_id,
|
||||
"ocr_text": "",
|
||||
"extracted_data": {},
|
||||
"message": "Файл загружен, но OCR не удалось"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Passport upload error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@@ -28,6 +28,15 @@ class Settings(BaseSettings):
|
||||
postgres_user: str = "gen_user"
|
||||
postgres_password: str = "2~~9_^kVsU?2\\S"
|
||||
|
||||
# ============================================
|
||||
# MYSQL (для проверки полисов ERV)
|
||||
# ============================================
|
||||
mysql_host: str = "localhost"
|
||||
mysql_port: int = 3306
|
||||
mysql_db: str = "u2768571_crm_db"
|
||||
mysql_user: str = "root"
|
||||
mysql_password: str = ""
|
||||
|
||||
@property
|
||||
def database_url(self) -> str:
|
||||
"""Формирует URL для подключения к PostgreSQL"""
|
||||
|
||||
@@ -10,7 +10,8 @@ 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
|
||||
from .services.policy_service import policy_service
|
||||
from .api import sms, claims, policy, upload
|
||||
|
||||
# Настройка логирования
|
||||
logging.basicConfig(
|
||||
@@ -80,6 +81,8 @@ app.add_middleware(
|
||||
# API Routes
|
||||
app.include_router(sms.router)
|
||||
app.include_router(claims.router)
|
||||
app.include_router(policy.router)
|
||||
app.include_router(upload.router)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
|
||||
89
backend/app/services/policy_service.py
Normal file
89
backend/app/services/policy_service.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
Policy Service - Проверка полисов в MySQL БД
|
||||
"""
|
||||
import aiomysql
|
||||
from typing import Optional, Dict, Any
|
||||
from ..config import settings
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PolicyService:
|
||||
"""Сервис для проверки полисов ERV"""
|
||||
|
||||
def __init__(self):
|
||||
self.pool: Optional[aiomysql.Pool] = None
|
||||
|
||||
async def connect(self):
|
||||
"""Подключение к MySQL БД с полисами"""
|
||||
try:
|
||||
# Используем credentials из .env через settings
|
||||
self.pool = await aiomysql.create_pool(
|
||||
host=settings.mysql_host,
|
||||
port=settings.mysql_port,
|
||||
user=settings.mysql_user,
|
||||
password=settings.mysql_password,
|
||||
db=settings.mysql_db,
|
||||
autocommit=True,
|
||||
minsize=1,
|
||||
maxsize=5
|
||||
)
|
||||
logger.info(f"✅ MySQL Policy DB connected: {settings.mysql_host}/{settings.mysql_db}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ MySQL Policy DB connection error: {e}")
|
||||
raise
|
||||
|
||||
async def check_policy(self, voucher: str, inn: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Проверить полис в БД
|
||||
|
||||
Args:
|
||||
voucher: Номер полиса
|
||||
inn: ИНН (опционально)
|
||||
|
||||
Returns:
|
||||
Dict с данными полиса или None если не найден
|
||||
"""
|
||||
if not self.pool:
|
||||
await self.connect()
|
||||
|
||||
try:
|
||||
async with self.pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.DictCursor) as cursor:
|
||||
# Базовый запрос
|
||||
query = "SELECT * FROM erv_vouchers WHERE voucher = %s"
|
||||
params = [voucher]
|
||||
|
||||
# Если указан ИНН, добавляем проверку
|
||||
if inn:
|
||||
query += " AND inn = %s"
|
||||
params.append(inn)
|
||||
|
||||
query += " LIMIT 1"
|
||||
|
||||
await cursor.execute(query, params)
|
||||
result = await cursor.fetchone()
|
||||
|
||||
if result:
|
||||
logger.info(f"✅ Policy found: {voucher}")
|
||||
return dict(result)
|
||||
else:
|
||||
logger.warning(f"⚠️ Policy not found: {voucher}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking policy: {e}")
|
||||
return None
|
||||
|
||||
async def close(self):
|
||||
"""Закрыть пул подключений"""
|
||||
if self.pool:
|
||||
self.pool.close()
|
||||
await self.pool.wait_closed()
|
||||
logger.info("MySQL Policy DB pool closed")
|
||||
|
||||
|
||||
# Глобальный экземпляр
|
||||
policy_service = PolicyService()
|
||||
|
||||
@@ -65,6 +65,12 @@ class SMSService:
|
||||
logger.warning("SMS отправка отключена в конфигурации")
|
||||
return False
|
||||
|
||||
# DEBUG MODE: Не отправляем реальные SMS, экономим бюджет
|
||||
if settings.debug or settings.app_env == "development":
|
||||
logger.info(f"🔧 DEBUG MODE: SMS to {phone} not sent (saving money!)")
|
||||
logger.info(f"📱 Message would be: {message}")
|
||||
return True
|
||||
|
||||
try:
|
||||
# Получаем актуальный токен
|
||||
token = await self._get_token()
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
# Core FastAPI
|
||||
fastapi==0.115.0
|
||||
uvicorn[standard]==0.32.0
|
||||
python-multipart==0.0.12
|
||||
python-multipart==0.0.20
|
||||
websockets==14.1
|
||||
|
||||
# Database
|
||||
sqlalchemy[asyncio]==2.0.35
|
||||
asyncpg==0.30.0
|
||||
aiomysql==0.2.0
|
||||
aiomysql==0.3.2
|
||||
pymysql==1.1.2
|
||||
alembic==1.14.0
|
||||
|
||||
# Redis & RabbitMQ
|
||||
|
||||
Reference in New Issue
Block a user