Files
aiform_dev/backend/app/services/redis_service.py
AI Assistant 02689e65db fix: Исправление загрузки документов и SQL запросов
- Исправлена потеря документов при обновлении черновика (SQL объединяет вместо перезаписи)
- Исправлено определение типа документа (приоритет field_label над field_name)
- Исправлены дубликаты в documents_meta и documents_uploaded
- Добавлена передача group_index с фронтенда для правильного field_name
- Исправлены все документы в таблице clpr_claim_documents с правильными field_name
- Обновлены SQL запросы: claimsave и claimsave_final для нового флоу
- Добавлена поддержка multi-file upload для одного документа
- Исправлены дубликаты в списке загруженных документов на фронтенде

Файлы:
- SQL: SQL_CLAIMSAVE_FIXED_NEW_FLOW.sql, SQL_CLAIMSAVE_FINAL_FIXED_NEW_FLOW_WITH_UPLOADED.sql
- n8n: N8N_CODE_PROCESS_UPLOADED_FILES_FIXED.js (поддержка group_index)
- Backend: documents.py (передача group_index в n8n)
- Frontend: StepWizardPlan.tsx (передача group_index, исправление дубликатов)
- Скрипты: fix_claim_documents_field_names.py, fix_documents_meta_duplicates.py

Результат: документы больше не теряются, имеют правильные типы и field_name
2025-11-26 19:54:51 +03:00

163 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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 publish(self, channel: str, message: str):
"""Публикация сообщения в канал Redis Pub/Sub"""
try:
subscribers_count = await self.client.publish(channel, message)
logger.info(
f"📢 Redis publish: channel={channel}, message_length={len(message)}, subscribers={subscribers_count}"
)
if subscribers_count == 0:
logger.warning(
f"⚠️ No subscribers on channel {channel}. Message published but no one is listening!"
)
return subscribers_count
except Exception as e:
logger.error(f"❌ Redis publish error: {e}")
raise
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()