docs: Обновлён лог сессии - добавлена вторая часть (умная форма Step 2)
This commit is contained in:
@@ -354,3 +354,710 @@ Channels: ocr_events:{claim_id}
|
|||||||
**Автор:** AI Assistant (Claude Sonnet 4.5)
|
**Автор:** AI Assistant (Claude Sonnet 4.5)
|
||||||
**Дата:** 28 октября 2025, 01:00 MSK
|
**Дата:** 28 октября 2025, 01:00 MSK
|
||||||
|
|
||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
# 📋 Лог сессии: Умная форма Step 2 с AI-обработкой документов
|
||||||
|
|
||||||
|
**Дата:** 28 октября 2025 (13:00 - 17:00 MSK)
|
||||||
|
**Задача:** Рефакторинг Step 2 в интеллектуальную форму с пошаговой загрузкой и AI-обработкой документов
|
||||||
|
**Статус:** ✅ Успешно завершено
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Основные задачи
|
||||||
|
|
||||||
|
### 1. ✅ Улучшение UX на Step 1 (Policy)
|
||||||
|
- Добавлены динамические кнопки в модалке OCR:
|
||||||
|
- **"Продолжить →"** при успешном распознавании → переход на Step 2
|
||||||
|
- **"Загрузить другой файл"** при ошибке → очистка и повтор
|
||||||
|
- Добавлен **DEV MODE** панель с кнопкой быстрого перехода на Step 2 без валидации
|
||||||
|
|
||||||
|
### 2. ✅ Рефакторинг Step 2 (Details)
|
||||||
|
**Было:**
|
||||||
|
- Ручной ввод всех полей (тип события, дата, номер рейса/поезда/парома)
|
||||||
|
- Загрузка документов как дополнение к ручному вводу
|
||||||
|
|
||||||
|
**Стало:**
|
||||||
|
- **"Интеллектуальная форма"** — AI извлекает данные из документов
|
||||||
|
- **Пошаговая загрузка** каждого документа с индивидуальной обработкой
|
||||||
|
- **Модалка обработки** для каждого документа с результатами извлечения
|
||||||
|
- Ручной ввод только при необходимости (fallback)
|
||||||
|
|
||||||
|
### 3. ✅ Определение требований к документам
|
||||||
|
|
||||||
|
#### Задержка рейса (`delay_flight`)
|
||||||
|
1. **Посадочный талон ИЛИ Билет** (обязательно)
|
||||||
|
- `file_type: flight_delay_boarding_or_ticket`
|
||||||
|
- `event_type: flight_delay_boarding_or_ticket_processed`
|
||||||
|
- AI извлекает: номер рейса, дату, маршрут, ФИО, время вылета
|
||||||
|
|
||||||
|
2. **Подтверждение задержки** (обязательно, до 3 файлов)
|
||||||
|
- `file_type: flight_delay_confirmation`
|
||||||
|
- `event_type: flight_delay_confirmation_processed`
|
||||||
|
- Справка от АК, email/SMS, ИЛИ фото табло
|
||||||
|
- AI извлекает: время задержки, причину, фактическое время вылета
|
||||||
|
|
||||||
|
#### Отмена рейса (`cancel_flight`)
|
||||||
|
1. **Билет** (обязательно)
|
||||||
|
- `file_type: flight_cancel_ticket`
|
||||||
|
- `event_type: flight_cancel_ticket_processed`
|
||||||
|
|
||||||
|
2. **Уведомление об отмене** (обязательно, до 3 файлов)
|
||||||
|
- `file_type: flight_cancel_notice`
|
||||||
|
- `event_type: flight_cancel_notice_processed`
|
||||||
|
- Письмо/SMS от АК, фото табло
|
||||||
|
|
||||||
|
#### Пропуск стыковки (`missed_connection`)
|
||||||
|
1. **Рейс отправления: Посадочный талон ИЛИ Билет** (обязательно)
|
||||||
|
- `file_type: missed_connection_first_boarding_or_ticket`
|
||||||
|
- `event_type: missed_connection_first_boarding_or_ticket_processed`
|
||||||
|
|
||||||
|
2. **Рейс прибытия: Билет на пропущенный рейс** (обязательно)
|
||||||
|
- `file_type: missed_connection_second_ticket`
|
||||||
|
- `event_type: missed_connection_second_ticket_processed`
|
||||||
|
|
||||||
|
3. **Подтверждение задержки первого рейса** (опционально, до 3 файлов)
|
||||||
|
- `file_type: missed_connection_delay_proof`
|
||||||
|
- `event_type: missed_connection_delay_proof_processed`
|
||||||
|
|
||||||
|
#### Задержка/отмена поезда (`delay_train`, `cancel_train`)
|
||||||
|
1. **Билет** (обязательно)
|
||||||
|
- `file_type: train_delay_ticket` / `train_cancel_ticket`
|
||||||
|
|
||||||
|
2. **Справка о задержке/отмене** (обязательно, до 3 файлов)
|
||||||
|
- `file_type: train_delay_certificate` / `train_cancel_certificate`
|
||||||
|
- Справка от ЖД, фото табло
|
||||||
|
|
||||||
|
#### Задержка/отмена парома/круиза (`delay_ferry`, `cancel_ferry`)
|
||||||
|
1. **Билет/Бронь** (обязательно)
|
||||||
|
- `file_type: ferry_delay_ticket` / `ferry_cancel_ticket`
|
||||||
|
|
||||||
|
2. **Подтверждение задержки/отмены** (обязательно, до 3 файлов)
|
||||||
|
- `file_type: ferry_delay_confirmation` / `ferry_cancel_confirmation`
|
||||||
|
- Справка, email, фото расписания
|
||||||
|
|
||||||
|
### 4. ✅ Уникальные `file_type` для каждого документа
|
||||||
|
|
||||||
|
**Принцип:** Каждый тип документа → уникальный `file_type` → уникальный `event_type` в Redis
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Пример для отмены рейса
|
||||||
|
{
|
||||||
|
file_type: "flight_cancel_ticket" // → S3, n8n, DB
|
||||||
|
event_type: "flight_cancel_ticket_processed" // → Redis pub/sub
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
file_type: "flight_cancel_notice"
|
||||||
|
event_type: "flight_cancel_notice_processed"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Почему это важно:**
|
||||||
|
- n8n разделяет потоки обработки по `file_type`
|
||||||
|
- Разные AI промпты для каждого типа документа
|
||||||
|
- Frontend слушает уникальный `event_type` для каждого документа
|
||||||
|
|
||||||
|
### 5. ✅ Добавлены DEV MODE кнопки во все 3 шага
|
||||||
|
|
||||||
|
**Step 1 (Policy):**
|
||||||
|
- "Далее → (Step 2) [пропустить]" — авто-заполнение voucher и claim_id
|
||||||
|
|
||||||
|
**Step 2 (Details):**
|
||||||
|
- "← Назад (Step 1)" — возврат назад
|
||||||
|
- "Далее → (Step 3) [пропустить]" — авто-заполнение eventType, incidentDate, transportNumber
|
||||||
|
|
||||||
|
**Step 3 (Payment):**
|
||||||
|
- "← Назад (Step 2)" — возврат назад
|
||||||
|
- "✅ Автоподтверждение телефона [dev]" — автозаполнение всех полей + setIsPhoneVerified(true)
|
||||||
|
- "🚀 Отправить [пропустить]" — автозаполнение + submit
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Технические изменения
|
||||||
|
|
||||||
|
### Файл: `frontend/src/components/form/Step1Policy.tsx`
|
||||||
|
|
||||||
|
#### Изменение 1: Динамические кнопки в модалке OCR
|
||||||
|
|
||||||
|
**Было:**
|
||||||
|
```typescript
|
||||||
|
footer={[
|
||||||
|
<Button key="close" onClick={() => setOcrModalVisible(false)}>
|
||||||
|
Закрыть
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Стало:**
|
||||||
|
```typescript
|
||||||
|
footer={ocrModalContent === 'loading' ? null :
|
||||||
|
ocrModalContent?.success ? [
|
||||||
|
<Button key="next" type="primary" onClick={() => {
|
||||||
|
setOcrModalVisible(false);
|
||||||
|
onNext(); // Переход на следующий шаг
|
||||||
|
}}>
|
||||||
|
Продолжить →
|
||||||
|
</Button>
|
||||||
|
] : [
|
||||||
|
<Button key="retry" type="primary" onClick={() => {
|
||||||
|
setOcrModalVisible(false);
|
||||||
|
setFileList([]); // Очищаем список файлов
|
||||||
|
setPolicyNotFound(true); // Показываем форму загрузки снова
|
||||||
|
}}>
|
||||||
|
Загрузить другой файл
|
||||||
|
</Button>
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Изменение 2: DEV MODE панель
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<div style={{
|
||||||
|
marginTop: 24,
|
||||||
|
padding: 16,
|
||||||
|
background: '#f0f0f0',
|
||||||
|
borderRadius: 8,
|
||||||
|
border: '2px dashed #999'
|
||||||
|
}}>
|
||||||
|
<div style={{ marginBottom: 8, fontSize: 12, color: '#666', fontWeight: 'bold' }}>
|
||||||
|
🔧 DEV MODE - Быстрая навигация (без валидации)
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
onClick={() => {
|
||||||
|
const devData = {
|
||||||
|
voucher: 'E1000-123456789',
|
||||||
|
claim_id: `CLM-DEV-${Math.random().toString(36).substr(2, 6).toUpperCase()}`,
|
||||||
|
};
|
||||||
|
updateFormData(devData);
|
||||||
|
onNext();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Далее → (Step 2) [пропустить]
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Файл: `frontend/src/components/form/Step2Details.tsx`
|
||||||
|
|
||||||
|
#### Полный рефакторинг!
|
||||||
|
|
||||||
|
**Бэкап старой версии:** `Step2Details.OLD_MANUAL_INPUT.tsx`
|
||||||
|
|
||||||
|
**Новая структура:**
|
||||||
|
|
||||||
|
1. **`DOCUMENT_CONFIGS`** — конфигурация документов для каждого типа события:
|
||||||
|
```typescript
|
||||||
|
const DOCUMENT_CONFIGS = {
|
||||||
|
delay_flight: [
|
||||||
|
{
|
||||||
|
name: "Посадочный талон ИЛИ Билет",
|
||||||
|
field: "boarding_or_ticket",
|
||||||
|
file_type: "flight_delay_boarding_or_ticket",
|
||||||
|
required: true,
|
||||||
|
maxFiles: 1,
|
||||||
|
description: "Посадочный талон (boarding pass) или билет (ticket/booking)",
|
||||||
|
aiPromptFocus: "Извлеки: номер рейса, дату, маршрут, ФИО пассажира, время вылета"
|
||||||
|
},
|
||||||
|
// ... остальные документы
|
||||||
|
],
|
||||||
|
cancel_flight: [...],
|
||||||
|
// ... остальные типы событий
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Пошаговая загрузка документов:**
|
||||||
|
```typescript
|
||||||
|
const [currentDocIndex, setCurrentDocIndex] = useState(0);
|
||||||
|
const currentDoc = requiredDocs[currentDocIndex];
|
||||||
|
|
||||||
|
// После успешной загрузки
|
||||||
|
if (currentDocIndex < requiredDocs.length - 1) {
|
||||||
|
setCurrentDocIndex(prev => prev + 1);
|
||||||
|
} else {
|
||||||
|
// Все документы загружены
|
||||||
|
onNext();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Модалка обработки для каждого документа:**
|
||||||
|
```typescript
|
||||||
|
<Modal
|
||||||
|
title="Обработка документа"
|
||||||
|
visible={processingModalVisible}
|
||||||
|
footer={processingModalContent === 'loading' ? null : [
|
||||||
|
<Button type="primary" onClick={handleContinueAfterProcessing}>
|
||||||
|
{currentDocIndex < requiredDocs.length - 1
|
||||||
|
? 'Продолжить к следующему документу →'
|
||||||
|
: 'Далее (Step 3) →'
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{processingModalContent === 'loading' ? (
|
||||||
|
<div style={{textAlign: 'center', padding: 24}}>
|
||||||
|
<Spin size="large" />
|
||||||
|
<p>Обрабатываем документ...</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<Alert type="success" message="✅ Документ обработан" />
|
||||||
|
<pre>{JSON.stringify(processingModalContent, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **SSE для каждого документа с уникальным `event_type`:**
|
||||||
|
```typescript
|
||||||
|
const eventSource = new EventSource(
|
||||||
|
`${API_BASE_URL}/events/${claimId}?event_type=${currentDoc.file_type}_processed`
|
||||||
|
);
|
||||||
|
|
||||||
|
eventSource.onmessage = (event) => {
|
||||||
|
const result = JSON.parse(event.data);
|
||||||
|
if (result.event_type === `${currentDoc.file_type}_processed`) {
|
||||||
|
setProcessingModalContent(result.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### DEV MODE панель:
|
||||||
|
```typescript
|
||||||
|
<div style={{ marginTop: 24, padding: 16, background: '#f0f0f0' }}>
|
||||||
|
<Button onClick={onPrev}>← Назад (Step 1)</Button>
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
onClick={() => {
|
||||||
|
const devData = { eventType: 'delay_flight', incidentDate: dayjs(), transportNumber: 'TEST123' };
|
||||||
|
updateFormData(devData);
|
||||||
|
onNext();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Далее → (Step 3) [пропустить]
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Файл: `frontend/src/components/form/Step3Payment.tsx`
|
||||||
|
|
||||||
|
#### DEV MODE панель (3 кнопки):
|
||||||
|
```typescript
|
||||||
|
<Button onClick={onPrev}>← Назад (Step 2)</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
onClick={() => {
|
||||||
|
setIsPhoneVerified(true);
|
||||||
|
const devData = {
|
||||||
|
fullName: 'Тест Тестов',
|
||||||
|
email: 'test@test.ru',
|
||||||
|
phone: '+79991234567',
|
||||||
|
paymentMethod: 'sbp',
|
||||||
|
bankName: 'sberbank',
|
||||||
|
};
|
||||||
|
updateFormData(devData);
|
||||||
|
message.success('DEV: Телефон автоматически подтверждён');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✅ Автоподтверждение телефона [dev]
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
setIsPhoneVerified(true);
|
||||||
|
const devData = {...};
|
||||||
|
updateFormData(devData);
|
||||||
|
onSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🚀 Отправить [пропустить]
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Проблемы и их решения
|
||||||
|
|
||||||
|
### Проблема 1: Синтаксические ошибки на фронте
|
||||||
|
|
||||||
|
**Симптом:**
|
||||||
|
```
|
||||||
|
чета шляпа у нас на фронте
|
||||||
|
```
|
||||||
|
|
||||||
|
**Диагностика:**
|
||||||
|
- Пользователь сообщил "что то не того"
|
||||||
|
- Проверка файлов показала **дублирующийся код** после закрывающих тегов компонентов
|
||||||
|
|
||||||
|
**Найденные проблемы:**
|
||||||
|
|
||||||
|
1. **`Step1Policy.tsx`** (строки 659-820):
|
||||||
|
- Дублирован весь DEV MODE блок после `</div>` компонента
|
||||||
|
- Код был просто скопирован повторно
|
||||||
|
|
||||||
|
2. **`Step3Payment.tsx`** (после строки 381):
|
||||||
|
- Дублирован обрезанный фрагмент DEV панели
|
||||||
|
- Неполный JSX
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
```bash
|
||||||
|
# Удалены дублирующиеся блоки
|
||||||
|
# Step1Policy.tsx: строки 659-820 удалены
|
||||||
|
# Step3Payment.tsx: строки после 381 удалены
|
||||||
|
|
||||||
|
# Rebuild frontend
|
||||||
|
docker-compose build frontend
|
||||||
|
docker-compose up -d frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
**Коммиты:**
|
||||||
|
- `2999951` - fix: Удалён дублирующийся код в Step1Policy.tsx
|
||||||
|
- `1207222` - fix: Удалён дублирующийся код в Step3Payment.tsx
|
||||||
|
|
||||||
|
### Проблема 2: PostgreSQL INSERT не возвращает данные в n8n
|
||||||
|
|
||||||
|
**Симптом:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"s3_url": null,
|
||||||
|
"file_id": null,
|
||||||
|
"error": {
|
||||||
|
"message": "422 - \"{\\\"detail\\\":[{\\\"type\\\":\\\"string_type\\\",\\\"loc\\\":[\\\"body\\\",\\\"file_url\\\"],\\\"msg\\\":\\\"Input should be a valid string\\\",\\\"input\\\":null}]}\""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Причина:**
|
||||||
|
1. `INSERT INTO claim_files` не вернул `file_id` и `s3_url`
|
||||||
|
2. Выяснилось: запись в `claims` с данным `claim_number` не существует
|
||||||
|
3. Foreign key `claim_id` не может быть установлен → INSERT падает
|
||||||
|
4. `file_size` передан как `"4.47 MB"` вместо числа в байтах
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
Создан UPSERT запрос с CTE (Common Table Expression):
|
||||||
|
|
||||||
|
```sql
|
||||||
|
WITH upserted_claim AS (
|
||||||
|
INSERT INTO claims (
|
||||||
|
claim_number, voucher, session_id, status, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, 'draft', NOW(), NOW()
|
||||||
|
)
|
||||||
|
ON CONFLICT (claim_number)
|
||||||
|
DO UPDATE SET
|
||||||
|
updated_at = NOW(),
|
||||||
|
voucher = COALESCE(EXCLUDED.voucher, claims.voucher)
|
||||||
|
RETURNING id as claim_id
|
||||||
|
)
|
||||||
|
INSERT INTO claim_files (
|
||||||
|
claim_id, file_type, original_name, s3_key, s3_url,
|
||||||
|
file_size, mime_type, ocr_status, uploaded_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
upserted_claim.claim_id,
|
||||||
|
$4, $5, $6, $7, $8, $9, 'pending', NOW()
|
||||||
|
FROM upserted_claim
|
||||||
|
RETURNING id as file_id, s3_url, ocr_status;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Параметры:**
|
||||||
|
```javascript
|
||||||
|
[
|
||||||
|
claim_number, // $1
|
||||||
|
voucher, // $2
|
||||||
|
session_id, // $3
|
||||||
|
file_type, // $4
|
||||||
|
original_name, // $5
|
||||||
|
s3_key, // $6
|
||||||
|
s3_url, // $7
|
||||||
|
file_size, // $8 (число в байтах!)
|
||||||
|
mime_type // $9
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Преимущества:**
|
||||||
|
- ✅ Атомарная операция
|
||||||
|
- ✅ Идемпотентность (можно запускать повторно)
|
||||||
|
- ✅ Всегда создаёт `claims` если его нет
|
||||||
|
- ✅ Обновляет `updated_at` если уже есть
|
||||||
|
- ✅ Возвращает `file_id` и `s3_url` для следующих шагов
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Тестирование
|
||||||
|
|
||||||
|
### Тест 1: Загрузка билета на отмену рейса
|
||||||
|
|
||||||
|
**Файл:** "Билет Романова.pdf"
|
||||||
|
**Claim ID:** CLM-2025-10-28-33ID32
|
||||||
|
**file_type:** `flight_cancel_ticket`
|
||||||
|
|
||||||
|
**Результат Redis:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"claim_id": "CLM-2025-10-28-33ID32",
|
||||||
|
"event": {
|
||||||
|
"event_type": "flight_cancel_ticket_processed",
|
||||||
|
"status": "completed",
|
||||||
|
"message": "✅ Документ обработан: flight_cancel_ticket",
|
||||||
|
"data": {
|
||||||
|
"output": {
|
||||||
|
"is_flight_doc": "yes",
|
||||||
|
"document_type": "e-ticket",
|
||||||
|
"ticket_number": "2222411714956",
|
||||||
|
"passengers": [{
|
||||||
|
"full_name": "ROMANOVA ANASTASIIA",
|
||||||
|
"doc_number": "774099576"
|
||||||
|
}],
|
||||||
|
"itinerary": [{
|
||||||
|
"flight_number": "A4-6025",
|
||||||
|
"departure": {
|
||||||
|
"airport_iata": "MRV",
|
||||||
|
"date_local": "2025-09-30",
|
||||||
|
"time_local": "16:25"
|
||||||
|
},
|
||||||
|
"arrival": {
|
||||||
|
"airport_iata": "TLV",
|
||||||
|
"time_local": "20:00"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Backend лог:**
|
||||||
|
```
|
||||||
|
16:46:29 - 📥 Received message type: message
|
||||||
|
16:46:29 - 📦 Raw event data: {"claim_id":"CLM-2025-10-28-33ID32",...}
|
||||||
|
16:46:29 - 📦 Unwrapped n8n Redis format for CLM-2025-10-28-33ID32
|
||||||
|
16:46:29 - 📤 Sending event to client: completed
|
||||||
|
16:46:29 - ✅ Task CLM-2025-10-28-33ID32 finished, closing SSE
|
||||||
|
```
|
||||||
|
|
||||||
|
**Результат:** ✅ Полный успех!
|
||||||
|
- S3 upload ✅
|
||||||
|
- PostgreSQL UPSERT ✅
|
||||||
|
- OCR/AI обработка ✅
|
||||||
|
- Redis publish ✅
|
||||||
|
- Backend SSE ✅
|
||||||
|
- Frontend получил данные ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Архитектура Step 2 (новая)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Step 2: Details (NEW) │
|
||||||
|
│ │
|
||||||
|
│ 1. Выбор типа события (eventType) │
|
||||||
|
│ ↓ │
|
||||||
|
│ 2. DOCUMENT_CONFIGS определяет список документов │
|
||||||
|
│ ↓ │
|
||||||
|
│ 3. Пошаговая загрузка каждого документа: │
|
||||||
|
│ │
|
||||||
|
│ ┌────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Документ 1: Посадочный талон │ │
|
||||||
|
│ │ - Upload компонент │ │
|
||||||
|
│ │ - POST /upload → n8n webhook │ │
|
||||||
|
│ │ - file_type: "flight_delay_boarding_or_ticket" │ │
|
||||||
|
│ │ - SSE: event_type = "..._processed" │ │
|
||||||
|
│ │ - Модалка: "Обрабатываем..." │ │
|
||||||
|
│ │ - Результат: extracted data │ │
|
||||||
|
│ │ - Кнопка: "Продолжить к следующему" │ │
|
||||||
|
│ └────────────────────────────────────────────────────┘ │
|
||||||
|
│ ↓ │
|
||||||
|
│ ┌────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Документ 2: Подтверждение задержки │ │
|
||||||
|
│ │ - (аналогично) │ │
|
||||||
|
│ │ - file_type: "flight_delay_confirmation" │ │
|
||||||
|
│ │ - Может быть до 3 файлов │ │
|
||||||
|
│ │ - Кнопка: "Далее (Step 3)" │ │
|
||||||
|
│ └────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ 4. После загрузки всех документов → Step 3 │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Flow для одного документа:
|
||||||
|
|
||||||
|
```
|
||||||
|
Frontend n8n Backend Redis
|
||||||
|
│ │ │ │
|
||||||
|
│ POST /upload │ │ │
|
||||||
|
├────────────────────>│ │ │
|
||||||
|
│ {claim_id, │ │ │
|
||||||
|
│ file_type, │ │ │
|
||||||
|
│ file} │ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ SSE connect │ │ │
|
||||||
|
├────────────────────────────────────────────>│ │
|
||||||
|
│ /events/CLM-XXX? │ │ │
|
||||||
|
│ event_type= │ │ │
|
||||||
|
│ flight_..._processed│ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 1. Upload to S3 │ │
|
||||||
|
│ │ 2. UPSERT claims │ │
|
||||||
|
│ │ 3. INSERT claim_files │ │
|
||||||
|
│ │ 4. OCR Service │ │
|
||||||
|
│ │ 5. AI Vision │ │
|
||||||
|
│ │ 6. PUBLISH │ │
|
||||||
|
│ ├────────────────────────────────────────────>│
|
||||||
|
│ │ {event_type: │ │
|
||||||
|
│ │ "..._processed", │ │
|
||||||
|
│ │ data: {...}} │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │<──────────────────┤
|
||||||
|
│ │ │ SUBSCRIBE │
|
||||||
|
│ │ │ ocr_events:CLM-XXX │
|
||||||
|
│<────────────────────────────────────────────┤ │
|
||||||
|
│ SSE: data: {event_type, data} │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ Show modal: │ │ │
|
||||||
|
│ "✅ Обработано" │ │ │
|
||||||
|
│ Display data │ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ User clicks │ │ │
|
||||||
|
│ "Continue" → │ │ │
|
||||||
|
│ next document │ │ │
|
||||||
|
│ (or Step 3) │ │ │
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Логика обработки результатов AI (спроектирована)
|
||||||
|
|
||||||
|
### Предложенная структура валидации:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const handleOcrResult = (event) => {
|
||||||
|
const { output } = event.data;
|
||||||
|
|
||||||
|
// Проверка 1: Это правильный тип документа?
|
||||||
|
if (output.is_flight_doc !== "yes") {
|
||||||
|
return { success: false, message: "❌ Это не авиадокумент" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка 2: Извлечены ли критичные данные?
|
||||||
|
const firstFlight = output.itinerary?.[0];
|
||||||
|
const criticalFields = {
|
||||||
|
flightNumber: firstFlight?.flight_number,
|
||||||
|
departureDate: firstFlight?.departure?.date_local,
|
||||||
|
departureAirport: firstFlight?.departure?.airport_iata,
|
||||||
|
arrivalAirport: firstFlight?.arrival?.airport_iata,
|
||||||
|
passengerName: output.passengers?.[0]?.full_name
|
||||||
|
};
|
||||||
|
|
||||||
|
const missing = Object.entries(criticalFields)
|
||||||
|
.filter(([_, value]) => !value)
|
||||||
|
.map(([key]) => key);
|
||||||
|
|
||||||
|
if (missing.length === 0) {
|
||||||
|
return { success: true, message: "✅ Билет распознан успешно!" };
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
success: "partial",
|
||||||
|
message: "⚠️ Билет распознан, но не хватает данных",
|
||||||
|
missingFields: missing
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3 сценария UI:
|
||||||
|
|
||||||
|
**SUCCESS:** Все данные извлечены
|
||||||
|
- ✅ Показать извлечённые данные
|
||||||
|
- Кнопка: "Продолжить к следующему документу →"
|
||||||
|
|
||||||
|
**PARTIAL:** Документ валидный, но данные неполные
|
||||||
|
- ⚠️ Показать что извлечено + что отсутствует
|
||||||
|
- 3 кнопки:
|
||||||
|
1. "📸 Загрузить документ лучшего качества"
|
||||||
|
2. "✍️ Ввести недостающие данные вручную"
|
||||||
|
3. "Продолжить с доступными данными"
|
||||||
|
|
||||||
|
**FAIL:** Неправильный тип документа
|
||||||
|
- ❌ Ошибка
|
||||||
|
- 2 кнопки:
|
||||||
|
1. "Загрузить другой файл"
|
||||||
|
2. "Ввести данные вручную"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Git Commits
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Commit history (от старого к новому)
|
||||||
|
6fe1459 - backup: Сохранён старый Step2Details с ручным вводом полей
|
||||||
|
122af07 - feat: Умная форма Step2 с автоматическим распознаванием документов
|
||||||
|
9084d75 - feat: Пошаговая загрузка документов с модалкой на Step 2
|
||||||
|
2999951 - fix: Удалён дублирующийся код в Step1Policy.tsx
|
||||||
|
1207222 - fix: Удалён дублирующийся код в Step3Payment.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
**Push:** ✅ `origin/main` (все коммиты)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Метрики
|
||||||
|
|
||||||
|
**Время выполнения сессии:** ~4 часа
|
||||||
|
**Количество коммитов:** 5
|
||||||
|
**Изменённых файлов:** 4 (Step1Policy, Step2Details, Step2Details.OLD, Step3Payment)
|
||||||
|
**Строк добавлено:** ~800
|
||||||
|
**Строк удалено:** ~200 (дубликаты) + ~400 (рефакторинг Step2)
|
||||||
|
|
||||||
|
**Frontend rebuilds:** 3
|
||||||
|
**Тестовых загрузок:** 3
|
||||||
|
**Redis событий обработано:** 3
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 Ссылки
|
||||||
|
|
||||||
|
- Frontend: http://147.45.146.17:5173
|
||||||
|
- Backend API: http://localhost:8100
|
||||||
|
- Gitea: http://147.45.146.17:3002/negodiy/erv-platform
|
||||||
|
- n8n: http://147.45.146.17:5678
|
||||||
|
- N8N SQL Queries: `/erv_platform/N8N_SQL_QUERIES.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Важные заметки
|
||||||
|
|
||||||
|
### Redis Password (обновлено)
|
||||||
|
```
|
||||||
|
Host: crm.clientright.ru
|
||||||
|
Port: 6379
|
||||||
|
Password: CRM_Redis_Pass_2025_Secure!
|
||||||
|
(из /etc/redis/redis.conf)
|
||||||
|
```
|
||||||
|
|
||||||
|
### PostgreSQL UPSERT для n8n
|
||||||
|
Сохранён в `N8N_SQL_QUERIES.md` для использования в webhook nodes.
|
||||||
|
|
||||||
|
### Структура `file_type` → `event_type`
|
||||||
|
```
|
||||||
|
file_type: "flight_cancel_ticket"
|
||||||
|
event_type: "flight_cancel_ticket_processed"
|
||||||
|
|
||||||
|
Формула: event_type = file_type + "_processed"
|
||||||
|
```
|
||||||
|
|
||||||
|
### DEV MODE
|
||||||
|
Все три шага имеют панель для быстрой навигации без заполнения форм — ускоряет разработку и тестирование.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Статус:** ✅ Успешно завершено
|
||||||
|
**Автор:** AI Assistant (Claude Sonnet 4.5)
|
||||||
|
**Дата:** 28 октября 2025, 17:00 MSK
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user