feat: Switch form approval to Redis channel instead of webhook
Changed from webhook to Redis Pub/Sub channel:
- Created endpoint POST /api/v1/claims/approve
- Publishes to Redis channel: form_approve:{claim_id}
- Added idempotency_key for future RabbitMQ integration
- Fire-and-forget approach (no waiting for response)
Benefits:
- Higher performance (Redis Pub/Sub is faster)
- Better scalability
- Ready for RabbitMQ queue integration
- Idempotency key included for duplicate protection
Files:
- backend/app/api/claims.py (new /approve endpoint)
- frontend/src/components/form/StepClaimConfirmation.tsx (updated saveFormData)
- docs/REDIS_FORM_APPROVE.md (documentation)
This commit is contained in:
@@ -542,32 +542,40 @@ async def load_wizard_data(claim_id: str):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/approve")
|
@router.post("/approve")
|
||||||
async def approve_claim_form(request: Request):
|
async def publish_form_approval(request: Request):
|
||||||
"""
|
"""
|
||||||
Сохранение данных подтвержденной формы после SMS-апрува
|
Публикация данных подтвержденной формы в Redis канал
|
||||||
|
|
||||||
Принимает отредактированные данные формы подтверждения и отправляет их в n8n webhook.
|
После SMS-апрува отправляет данные формы в Redis канал form_approve:{claim_id}
|
||||||
|
для обработки в n8n workflow.
|
||||||
|
|
||||||
|
В будущем можно подключить RabbitMQ для очереди и защиты от дублей.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
body = await request.json()
|
body = await request.json()
|
||||||
|
|
||||||
logger.info(
|
claim_id = body.get("claim_id")
|
||||||
"📨 TicketForm approval received",
|
session_token = body.get("session_token") or body.get("session_id")
|
||||||
extra={
|
|
||||||
"claim_id": body.get("claim_id"),
|
|
||||||
"session_id": body.get("session_id"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Формируем payload для n8n
|
if not claim_id:
|
||||||
n8n_payload = {
|
raise HTTPException(status_code=400, detail="claim_id обязателен")
|
||||||
"stage": "form_approve",
|
|
||||||
"form_id": "ticket_form",
|
# Генерируем idempotency key для защиты от дублей (для будущей интеграции с RabbitMQ)
|
||||||
"session_id": body.get("session_id"),
|
import time
|
||||||
"claim_id": body.get("claim_id"),
|
idempotency_key = f"{claim_id}_{int(time.time() * 1000)}_{body.get('user_id', 'unknown')}"
|
||||||
|
|
||||||
|
# Формируем событие для Redis
|
||||||
|
event_data = {
|
||||||
|
"event_type": "form_approve",
|
||||||
|
"status": "approved",
|
||||||
|
"message": "Форма подтверждена после SMS-верификации",
|
||||||
|
"claim_id": claim_id,
|
||||||
|
"session_token": session_token,
|
||||||
"unified_id": body.get("unified_id"),
|
"unified_id": body.get("unified_id"),
|
||||||
"phone": body.get("phone"),
|
"phone": body.get("phone"),
|
||||||
"sms_verified": True, # Флаг что SMS код подтвержден
|
"sms_verified": True,
|
||||||
|
"idempotency_key": idempotency_key, # Для защиты от дублей в RabbitMQ
|
||||||
|
"timestamp": datetime.utcnow().isoformat(),
|
||||||
|
|
||||||
# Данные формы подтверждения
|
# Данные формы подтверждения
|
||||||
"form_data": body.get("form_data", {}),
|
"form_data": body.get("form_data", {}),
|
||||||
@@ -580,50 +588,33 @@ async def approve_claim_form(request: Request):
|
|||||||
"original_data": body.get("original_data", {}),
|
"original_data": body.get("original_data", {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Проксируем запрос к n8n
|
# Публикуем в Redis канал form_approve:{claim_id}
|
||||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
channel = f"form_approve:{claim_id}"
|
||||||
response = await client.post(
|
event_json = json.dumps(event_data, ensure_ascii=False)
|
||||||
N8N_FORM_APPROVE_WEBHOOK,
|
await redis_service.publish(channel, event_json)
|
||||||
json=n8n_payload,
|
|
||||||
headers={"Content-Type": "application/json"},
|
|
||||||
)
|
|
||||||
|
|
||||||
text = response.text or ""
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"✅ TicketForm approval webhook OK",
|
f"📢 Form approval published to {channel}",
|
||||||
extra={"response_preview": text[:500]},
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
return json.loads(text)
|
|
||||||
except Exception:
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"message": "Form approval processed (non-JSON response from n8n)",
|
|
||||||
"raw": text,
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error(
|
|
||||||
"❌ TicketForm approval webhook error",
|
|
||||||
extra={
|
extra={
|
||||||
"status_code": response.status_code,
|
"claim_id": claim_id,
|
||||||
"body": text[:500],
|
"idempotency_key": idempotency_key,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
|
||||||
status_code=response.status_code,
|
|
||||||
detail=f"n8n error: {text}",
|
|
||||||
)
|
|
||||||
|
|
||||||
except httpx.TimeoutException:
|
return {
|
||||||
logger.error("⏱️ n8n approval webhook timeout")
|
"success": True,
|
||||||
raise HTTPException(status_code=504, detail="Таймаут подключения к n8n")
|
"channel": channel,
|
||||||
|
"idempotency_key": idempotency_key,
|
||||||
|
"message": "Данные формы отправлены на обработку",
|
||||||
|
}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("❌ Ошибка при сохранении подтвержденной формы")
|
logger.exception("❌ Failed to publish form approval")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500,
|
status_code=500,
|
||||||
detail=f"Ошибка при сохранении подтвержденной формы: {str(e)}",
|
detail=f"Ошибка при отправке данных формы: {str(e)}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
164
docs/REDIS_FORM_APPROVE.md
Normal file
164
docs/REDIS_FORM_APPROVE.md
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# Redis канал для подтверждения формы (form_approve)
|
||||||
|
|
||||||
|
## 📋 Описание
|
||||||
|
|
||||||
|
После SMS-апрува данные подтвержденной формы публикуются в Redis канал `form_approve:{claim_id}` для обработки в n8n workflow.
|
||||||
|
|
||||||
|
## 🔄 Архитектура
|
||||||
|
|
||||||
|
```
|
||||||
|
Frontend (StepClaimConfirmation)
|
||||||
|
→ POST /api/v1/claims/approve
|
||||||
|
→ Backend публикует в Redis канал form_approve:{claim_id}
|
||||||
|
→ n8n подписывается на канал и обрабатывает данные
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📡 Endpoint
|
||||||
|
|
||||||
|
**POST** `/api/v1/claims/approve`
|
||||||
|
|
||||||
|
### Request Body
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"claim_id": "0eb051ec-23a6-4e06-8b98-f02d20d35f68",
|
||||||
|
"session_token": "sess_c9e7c0c2-de2e-40cd-ab7c-3bdc40282d34",
|
||||||
|
"session_id": "sess_c9e7c0c2-de2e-40cd-ab7c-3bdc40282d34",
|
||||||
|
"unified_id": "usr_90599ff2-ac79-4236-b950-0df85395096c",
|
||||||
|
"user_id": "user_123",
|
||||||
|
"phone": "79262306381",
|
||||||
|
"form_data": {
|
||||||
|
"user": {...},
|
||||||
|
"project": {...},
|
||||||
|
"offenders": [...],
|
||||||
|
"meta": {...}
|
||||||
|
},
|
||||||
|
"user": {...},
|
||||||
|
"project": {...},
|
||||||
|
"offenders": [...],
|
||||||
|
"meta": {...},
|
||||||
|
"original_data": {...}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"channel": "form_approve:0eb051ec-23a6-4e06-8b98-f02d20d35f68",
|
||||||
|
"idempotency_key": "0eb051ec-23a6-4e06-8b98-f02d20d35f68_1735123456789_user_123",
|
||||||
|
"message": "Данные формы отправлены на обработку"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📢 Redis канал
|
||||||
|
|
||||||
|
**Канал:** `form_approve:{claim_id}`
|
||||||
|
|
||||||
|
**Формат сообщения:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_type": "form_approve",
|
||||||
|
"status": "approved",
|
||||||
|
"message": "Форма подтверждена после SMS-верификации",
|
||||||
|
"claim_id": "0eb051ec-23a6-4e06-8b98-f02d20d35f68",
|
||||||
|
"session_token": "sess_c9e7c0c2-de2e-40cd-ab7c-3bdc40282d34",
|
||||||
|
"unified_id": "usr_90599ff2-ac79-4236-b950-0df85395096c",
|
||||||
|
"phone": "79262306381",
|
||||||
|
"sms_verified": true,
|
||||||
|
"idempotency_key": "0eb051ec-23a6-4e06-8b98-f02d20d35f68_1735123456789_user_123",
|
||||||
|
"timestamp": "2025-11-24T14:15:26.03297+03:00",
|
||||||
|
"form_data": {...},
|
||||||
|
"user": {...},
|
||||||
|
"project": {...},
|
||||||
|
"offenders": [...],
|
||||||
|
"meta": {...},
|
||||||
|
"original_data": {...}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 Idempotency Key
|
||||||
|
|
||||||
|
Для защиты от дублей генерируется `idempotency_key`:
|
||||||
|
```
|
||||||
|
{claim_id}_{timestamp_ms}_{user_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
Этот ключ можно использовать в будущем для интеграции с RabbitMQ:
|
||||||
|
- Проверка дублей перед обработкой
|
||||||
|
- Дедупликация в очереди
|
||||||
|
- Гарантия идемпотентности
|
||||||
|
|
||||||
|
## 🚀 Настройка n8n
|
||||||
|
|
||||||
|
### 1. Redis Subscribe Node
|
||||||
|
|
||||||
|
**Operation:** `Subscribe`
|
||||||
|
**Channel:** `form_approve:{{ $json.claim_id }}`
|
||||||
|
**Или:** Подписка на паттерн `form_approve:*`
|
||||||
|
|
||||||
|
### 2. Обработка события
|
||||||
|
|
||||||
|
После получения события из Redis:
|
||||||
|
1. Проверить `idempotency_key` (для защиты от дублей)
|
||||||
|
2. Обработать данные формы
|
||||||
|
3. Сохранить в БД через SQL запрос
|
||||||
|
4. Отправить уведомления (если нужно)
|
||||||
|
|
||||||
|
### 3. Пример workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
[Redis Subscribe] → [Check Idempotency] → [Process Form Data] → [Save to DB] → [Send Notifications]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔮 Будущая интеграция с RabbitMQ
|
||||||
|
|
||||||
|
При необходимости можно подключить RabbitMQ для:
|
||||||
|
- **Очереди:** Гарантированная обработка всех событий
|
||||||
|
- **Защита от дублей:** Проверка `idempotency_key` перед добавлением в очередь
|
||||||
|
- **Retry механизм:** Автоматические повторы при ошибках
|
||||||
|
- **Масштабирование:** Несколько воркеров для обработки
|
||||||
|
|
||||||
|
### Структура для RabbitMQ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"queue": "form_approve",
|
||||||
|
"message": {
|
||||||
|
"idempotency_key": "...",
|
||||||
|
"claim_id": "...",
|
||||||
|
"data": {...}
|
||||||
|
},
|
||||||
|
"headers": {
|
||||||
|
"idempotency-key": "...",
|
||||||
|
"retry-count": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Мониторинг
|
||||||
|
|
||||||
|
### Проверка канала в Redis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
redis-cli -h crm.clientright.ru -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' \
|
||||||
|
MONITOR | grep form_approve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Подписка на канал (тест)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
redis-cli -h crm.clientright.ru -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' \
|
||||||
|
SUBSCRIBE form_approve:*
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Преимущества
|
||||||
|
|
||||||
|
1. **Высокая производительность:** Redis Pub/Sub очень быстрый
|
||||||
|
2. **Не блокирует фронтенд:** Fire-and-forget подход
|
||||||
|
3. **Масштабируемость:** Можно добавить несколько подписчиков
|
||||||
|
4. **Готовность к RabbitMQ:** Idempotency key уже включен
|
||||||
|
5. **Простота отладки:** Можно мониторить через Redis MONITOR
|
||||||
|
|
||||||
@@ -92,28 +92,27 @@ export default function StepClaimConfirmation({
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [claimPlanData]);
|
}, [claimPlanData]);
|
||||||
|
|
||||||
// Функция сохранения данных формы - отправка в webhook без ожидания ответа
|
// Функция сохранения данных формы - публикация в Redis канал
|
||||||
const saveFormData = useCallback(async (formData: any) => {
|
const saveFormData = useCallback(async (formData: any) => {
|
||||||
console.log('💾 Отправляем данные формы в webhook:', formData);
|
console.log('💾 Публикуем данные формы в Redis канал:', formData);
|
||||||
|
|
||||||
// Получаем данные из claimPlanData для формирования payload
|
// Получаем данные из claimPlanData для формирования payload
|
||||||
const claimId = claimPlanData?.claim_id || claimPlanData?.propertyName?.meta?.claim_id || '';
|
const claimId = claimPlanData?.claim_id || claimPlanData?.propertyName?.meta?.claim_id || '';
|
||||||
const unifiedId = claimPlanData?.unified_id || claimPlanData?.propertyName?.meta?.unified_id || '';
|
const unifiedId = claimPlanData?.unified_id || claimPlanData?.propertyName?.meta?.unified_id || '';
|
||||||
const sessionToken = claimPlanData?.session_token || '';
|
const sessionToken = claimPlanData?.session_token || '';
|
||||||
|
const userId = claimPlanData?.user_id || claimPlanData?.propertyName?.meta?.user_id || '';
|
||||||
const phone = claimPlanData?.propertyName?.applicant?.phone ||
|
const phone = claimPlanData?.propertyName?.applicant?.phone ||
|
||||||
claimPlanData?.propertyName?.user?.mobile ||
|
claimPlanData?.propertyName?.user?.mobile ||
|
||||||
claimPlanData?.phone || '';
|
claimPlanData?.phone || '';
|
||||||
|
|
||||||
// Формируем payload для webhook
|
// Формируем payload для Redis канала
|
||||||
const payload = {
|
const payload = {
|
||||||
stage: 'form_approve',
|
|
||||||
form_id: 'ticket_form',
|
|
||||||
session_id: sessionToken,
|
|
||||||
session_token: sessionToken,
|
|
||||||
claim_id: claimId,
|
claim_id: claimId,
|
||||||
|
session_token: sessionToken,
|
||||||
|
session_id: sessionToken,
|
||||||
unified_id: unifiedId,
|
unified_id: unifiedId,
|
||||||
|
user_id: userId,
|
||||||
phone: phone,
|
phone: phone,
|
||||||
sms_verified: true, // Флаг что SMS код подтвержден
|
|
||||||
|
|
||||||
// Данные формы подтверждения
|
// Данные формы подтверждения
|
||||||
form_data: formData,
|
form_data: formData,
|
||||||
@@ -126,9 +125,9 @@ export default function StepClaimConfirmation({
|
|||||||
original_data: formData?.originalData || {},
|
original_data: formData?.originalData || {},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Отправляем в webhook без ожидания ответа (fire-and-forget)
|
// Публикуем в Redis канал через backend endpoint (fire-and-forget)
|
||||||
// Используем fetch с keepalive для надежности, но не ждем ответа
|
// Канал: form_approve:{claim_id}
|
||||||
fetch('https://n8n.clientright.pro/webhook/eebe58d4-0bcd-4d09-9d62-39868b110960', {
|
fetch('/api/v1/claims/approve', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -137,10 +136,10 @@ export default function StepClaimConfirmation({
|
|||||||
keepalive: true, // Продолжить отправку даже если страница закрывается
|
keepalive: true, // Продолжить отправку даже если страница закрывается
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
// Тихо логируем ошибки, но не блокируем пользователя
|
// Тихо логируем ошибки, но не блокируем пользователя
|
||||||
console.error('Ошибка отправки данных формы в webhook:', error);
|
console.error('Ошибка публикации данных формы в Redis:', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('✅ Данные формы отправлены в webhook (fire-and-forget)');
|
console.log('✅ Данные формы опубликованы в Redis канал form_approve:', claimId);
|
||||||
}, [claimPlanData]);
|
}, [claimPlanData]);
|
||||||
|
|
||||||
// Функция отправки SMS-кода
|
// Функция отправки SMS-кода
|
||||||
|
|||||||
Reference in New Issue
Block a user