32 KiB
Документация изменений API для фронтенда
Обзор изменений
В API маркетингового анализа были внесены изменения для поддержки множественного выбора в трех полях:
- Целевая аудитория (
targetAudience) - теперь структурированный JSON объект вместо строки - Тип анализа (
analysisType) - теперь массив строк вместо одной строки - Регион (
region) - теперь массив строк вместо одной строки
Важно: При выборе нескольких типов анализа API создает отдельные записи анализа для каждого типа. В ответе возвращается массив анализов.
1. Изменения в поле targetAudience
Старый формат (устарел)
targetAudience: string;
"targetAudience": "Малый и средний бизнес, владельцы интернет-магазинов в возрасте 30-50 лет"
Новый формат
targetAudience: {
genders?: ('Женщины' | 'Мужчины')[];
ageRanges?: ('20-40' | '25-45' | '18-25' | '40-60' | '60+')[];
types?: ('Семьи' | 'Молодёжь' | 'Все подряд')[];
}
Примеры
Минимальный выбор (только гендер):
{
"targetAudience": {
"genders": ["Женщины"]
}
}
Полный выбор:
{
"targetAudience": {
"genders": ["Женщины", "Мужчины"],
"ageRanges": ["20-40", "25-45"],
"types": ["Семьи", "Молодёжь"]
}
}
Только возраст и тип:
{
"targetAudience": {
"ageRanges": ["25-45"],
"types": ["Молодёжь"]
}
}
Валидация
- Хотя бы одно поле (
genders,ageRanges,types) должно быть заполнено - Каждое поле должно содержать непустой массив
- Все значения должны быть из допустимого списка
Допустимые значения
- genders:
"Женщины","Мужчины" - ageRanges:
"20-40","25-45","18-25","40-60","60+" - types:
"Семьи","Молодёжь","Все подряд"
2. Изменения в поле analysisType
Старый формат (устарел)
analysisType: 'РЫНОК' | 'КОНКУРЕНТЫ' | 'ЦА' | 'КАНАЛЫ' | 'SWOT';
"analysisType": "РЫНОК"
Новый формат
analysisType: ('РЫНОК' | 'КОНКУРЕНТЫ' | 'ЦА' | 'КАНАЛЫ' | 'SWOT')[]
Примеры
Один тип анализа:
{
"analysisType": ["РЫНОК"]
}
Несколько типов анализа:
{
"analysisType": ["РЫНОК", "КОНКУРЕНТЫ", "ЦА"]
}
Все типы анализа:
{
"analysisType": ["РЫНОК", "КОНКУРЕНТЫ", "ЦА", "КАНАЛЫ", "SWOT"]
}
Важно: Поведение при множественном выборе
При выборе нескольких типов анализа:
- API создает отдельную запись анализа для каждого типа
- Каждый анализ обрабатывается независимо
- В ответе возвращается массив с информацией о каждом созданном анализе
- Каждый анализ имеет свой
analysisIdи статус
Валидация
- Массив должен содержать минимум один элемент
- Все значения должны быть из допустимого списка
Допустимые значения
"РЫНОК"- Анализ рынка"КОНКУРЕНТЫ"- Анализ конкурентов"ЦА"- Анализ целевой аудитории"КАНАЛЫ"- Анализ маркетинговых каналов"SWOT"- SWOT-анализ
3. Изменения в поле region
Старый формат (устарел)
region: string;
"region": "Алматы"
Новый формат
region: string[]
Примеры
Один регион:
{
"region": ["Алматы"]
}
Несколько регионов:
{
"region": ["Алматы", "Астана", "Шымкент"]
}
Все регионы:
{
"region": [
"Алматы",
"Астана",
"Шымкент",
"Караганда",
"Актобе",
"Тараз",
"Павлодар",
"Усть-Каменогорск",
"Семей",
"Костанай",
"Кызылорда",
"Уральск",
"Петропавловск",
"Атырау",
"Актау",
"Туркестан",
"Кокшетау",
"Талдыкорган",
"Экибастуз",
"Рудный"
]
}
Валидация
- Массив должен содержать минимум один элемент
- Все значения должны быть из допустимого списка городов Казахстана
Допустимые значения
Всего 20 городов:
"Алматы","Астана","Шымкент","Караганда","Актобе","Тараз","Павлодар","Усть-Каменогорск","Семей","Костанай","Кызылорда","Уральск","Петропавловск","Атырау","Актау","Туркестан","Кокшетау","Талдыкорган","Экибастуз","Рудный"
4. Изменения в ответе API
Эндпоинт: POST /api/marketing/analysis/start
Старый формат ответа (один анализ)
{
"success": true,
"message": "Анализ запущен успешно",
"data": {
"analysisId": "507f1f77bcf86cd799439011",
"status": "processing",
"estimatedCompletion": "2024-01-15T10:30:00",
"message": "Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут."
}
}
Новый формат ответа
Если выбран один тип анализа:
{
"success": true,
"message": "Анализ запущен успешно",
"data": {
"analysisId": "507f1f77bcf86cd799439011",
"status": "processing",
"estimatedCompletion": "2024-01-15T10:30:00",
"message": "Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут."
}
}
Если выбрано несколько типов анализа:
{
"success": true,
"message": "Создано анализов: 3. Все анализы запущены успешно.",
"data": [
{
"analysisId": "507f1f77bcf86cd799439011",
"status": "processing",
"estimatedCompletion": "2024-01-15T10:30:00",
"message": "Анализ типа 'РЫНОК' запущен успешно. Результаты будут готовы в течение 5-10 минут."
},
{
"analysisId": "507f1f77bcf86cd799439012",
"status": "processing",
"estimatedCompletion": "2024-01-15T10:30:00",
"message": "Анализ типа 'КОНКУРЕНТЫ' запущен успешно. Результаты будут готовы в течение 5-10 минут."
},
{
"analysisId": "507f1f77bcf86cd799439013",
"status": "processing",
"estimatedCompletion": "2024-01-15T10:30:00",
"message": "Анализ типа 'ЦА' запущен успешно. Результаты будут готовы в течение 5-10 минут."
}
]
}
5. Полный пример запроса
Пример 1: Один тип анализа
{
"businessNiche": "E-commerce платформы",
"product": "Разработка мобильных приложений",
"targetAudience": {
"genders": ["Женщины"],
"ageRanges": ["25-45"],
"types": ["Молодёжь"]
},
"region": ["Алматы"],
"goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев",
"detailLevel": "СТАНДАРТНО",
"strongSide": "Опытная команда, быстрая доставка",
"weakSide": "Ограниченный маркетинговый бюджет",
"analysisType": ["РЫНОК"]
}
Пример 2: Несколько типов анализа
{
"businessNiche": "E-commerce платформы",
"product": "Разработка мобильных приложений",
"targetAudience": {
"genders": ["Женщины", "Мужчины"],
"ageRanges": ["25-45", "40-60"],
"types": ["Семьи", "Молодёжь"]
},
"region": ["Алматы", "Астана", "Шымкент"],
"goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев",
"detailLevel": "СТАНДАРТНО",
"strongSide": "Опытная команда, быстрая доставка",
"weakSide": "Ограниченный маркетинговый бюджет",
"analysisType": ["РЫНОК", "КОНКУРЕНТЫ", "ЦА"]
}
6. TypeScript интерфейсы
// Интерфейс для целевой аудитории
interface TargetAudience {
genders?: ('Женщины' | 'Мужчины')[];
ageRanges?: ('20-40' | '25-45' | '18-25' | '40-60' | '60+')[];
types?: ('Семьи' | 'Молодёжь' | 'Все подряд')[];
}
// Интерфейс запроса
interface MarketingAnalysisRequest {
businessNiche: string;
product: string;
targetAudience: TargetAudience;
region: string[];
goal: string;
detailLevel: 'КРАТКО' | 'СТАНДАРТНО' | 'ПОДРОБНО';
strongSide?: string;
weakSide?: string;
analysisType: ('РЫНОК' | 'КОНКУРЕНТЫ' | 'ЦА' | 'КАНАЛЫ' | 'SWOT')[];
}
// Интерфейс ответа
interface MarketingAnalysisResponse {
analysisId: string;
status: string;
estimatedCompletion: string;
message: string;
}
// Тип ответа API (может быть один объект или массив)
type StartAnalysisResponse =
| MarketingAnalysisResponse
| MarketingAnalysisResponse[];
7. Примеры реализации на фронтенде
React компонент с формой
import React, { useState } from 'react';
const VALID_GENDERS = ['Женщины', 'Мужчины'] as const;
const VALID_AGE_RANGES = ['20-40', '25-45', '18-25', '40-60', '60+'] as const;
const VALID_TYPES = ['Семьи', 'Молодёжь', 'Все подряд'] as const;
const VALID_ANALYSIS_TYPES = [
'РЫНОК',
'КОНКУРЕНТЫ',
'ЦА',
'КАНАЛЫ',
'SWOT',
] as const;
const VALID_REGIONS = [
'Алматы',
'Астана',
'Шымкент',
'Караганда',
'Актобе',
'Тараз',
'Павлодар',
'Усть-Каменогорск',
'Семей',
'Костанай',
'Кызылорда',
'Уральск',
'Петропавловск',
'Атырау',
'Актау',
'Туркестан',
'Кокшетау',
'Талдыкорган',
'Экибастуз',
'Рудный',
] as const;
const MarketingAnalysisForm: React.FC = () => {
const [formData, setFormData] = useState<MarketingAnalysisRequest>({
businessNiche: '',
product: '',
targetAudience: {
genders: [],
ageRanges: [],
types: [],
},
region: [],
goal: '',
detailLevel: 'СТАНДАРТНО',
strongSide: '',
weakSide: '',
analysisType: [],
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Валидация: проверяем, что выбрана хотя бы одна опция в каждом обязательном поле
if (
formData.targetAudience.genders?.length === 0 &&
formData.targetAudience.ageRanges?.length === 0 &&
formData.targetAudience.types?.length === 0
) {
alert('Выберите хотя бы одну опцию для целевой аудитории');
return;
}
if (formData.region.length === 0) {
alert('Выберите хотя бы один регион');
return;
}
if (formData.analysisType.length === 0) {
alert('Выберите хотя бы один тип анализа');
return;
}
try {
const response = await fetch('/api/marketing/analysis/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${getAuthToken()}`,
},
body: JSON.stringify(formData),
});
const result = await response.json();
if (!result.success) {
throw new Error(result.error?.message || 'Ошибка при создании анализа');
}
// Обработка ответа (может быть массив или один объект)
if (Array.isArray(result.data)) {
console.log(`Создано ${result.data.length} анализов`);
result.data.forEach(
(analysis: MarketingAnalysisResponse, index: number) => {
console.log(`Анализ ${index + 1}: ${analysis.analysisId}`);
// Перенаправление на страницу с результатами для каждого анализа
}
);
} else {
console.log('Анализ создан:', result.data.analysisId);
// Перенаправление на страницу с результатами
}
} catch (error) {
console.error('Ошибка:', error);
alert('Не удалось создать анализ. Попробуйте позже.');
}
};
return (
<form onSubmit={handleSubmit}>
{/* Поле: Целевая аудитория */}
<div>
<label>Целевая аудитория *</label>
<div>
<label>Гендер:</label>
{VALID_GENDERS.map((gender) => (
<label key={gender}>
<input
type='checkbox'
checked={
formData.targetAudience.genders?.includes(gender) || false
}
onChange={(e) => {
const genders = formData.targetAudience.genders || [];
if (e.target.checked) {
setFormData({
...formData,
targetAudience: {
...formData.targetAudience,
genders: [...genders, gender],
},
});
} else {
setFormData({
...formData,
targetAudience: {
...formData.targetAudience,
genders: genders.filter((g) => g !== gender),
},
});
}
}}
/>
{gender}
</label>
))}
</div>
<div>
<label>Возраст:</label>
{VALID_AGE_RANGES.map((age) => (
<label key={age}>
<input
type='checkbox'
checked={
formData.targetAudience.ageRanges?.includes(age) || false
}
onChange={(e) => {
const ageRanges = formData.targetAudience.ageRanges || [];
if (e.target.checked) {
setFormData({
...formData,
targetAudience: {
...formData.targetAudience,
ageRanges: [...ageRanges, age],
},
});
} else {
setFormData({
...formData,
targetAudience: {
...formData.targetAudience,
ageRanges: ageRanges.filter((a) => a !== age),
},
});
}
}}
/>
{age}
</label>
))}
</div>
<div>
<label>Тип:</label>
{VALID_TYPES.map((type) => (
<label key={type}>
<input
type='checkbox'
checked={formData.targetAudience.types?.includes(type) || false}
onChange={(e) => {
const types = formData.targetAudience.types || [];
if (e.target.checked) {
setFormData({
...formData,
targetAudience: {
...formData.targetAudience,
types: [...types, type],
},
});
} else {
setFormData({
...formData,
targetAudience: {
...formData.targetAudience,
types: types.filter((t) => t !== type),
},
});
}
}}
/>
{type}
</label>
))}
</div>
{/* Отображение выбранных значений */}
<div>
<strong>Выбрано:</strong>
{formData.targetAudience.genders &&
formData.targetAudience.genders.length > 0 && (
<span> Гендер: {formData.targetAudience.genders.join(', ')}</span>
)}
{formData.targetAudience.ageRanges &&
formData.targetAudience.ageRanges.length > 0 && (
<span>
{' '}
Возраст: {formData.targetAudience.ageRanges.join(', ')}
</span>
)}
{formData.targetAudience.types &&
formData.targetAudience.types.length > 0 && (
<span> Тип: {formData.targetAudience.types.join(', ')}</span>
)}
</div>
</div>
{/* Поле: Регион */}
<div>
<label>Регион *</label>
<div>
<label>
<input
type='checkbox'
checked={formData.region.length === VALID_REGIONS.length}
onChange={(e) => {
if (e.target.checked) {
setFormData({
...formData,
region: [...VALID_REGIONS],
});
} else {
setFormData({
...formData,
region: [],
});
}
}}
/>
Все регионы
</label>
</div>
<div>
{VALID_REGIONS.map((region) => (
<label key={region}>
<input
type='checkbox'
checked={formData.region.includes(region)}
onChange={(e) => {
if (e.target.checked) {
setFormData({
...formData,
region: [...formData.region, region],
});
} else {
setFormData({
...formData,
region: formData.region.filter((r) => r !== region),
});
}
}}
/>
{region}
</label>
))}
</div>
{/* Отображение выбранных регионов */}
<div>
<strong>Выбрано регионов: {formData.region.length}</strong>
{formData.region.length > 0 && (
<div>{formData.region.join(', ')}</div>
)}
</div>
</div>
{/* Поле: Тип анализа */}
<div>
<label>Тип анализа *</label>
<div>
<label>
<input
type='checkbox'
checked={
formData.analysisType.length === VALID_ANALYSIS_TYPES.length
}
onChange={(e) => {
if (e.target.checked) {
setFormData({
...formData,
analysisType: [...VALID_ANALYSIS_TYPES],
});
} else {
setFormData({
...formData,
analysisType: [],
});
}
}}
/>
Все типы анализа
</label>
</div>
<div>
{VALID_ANALYSIS_TYPES.map((type) => (
<label key={type}>
<input
type='checkbox'
checked={formData.analysisType.includes(type)}
onChange={(e) => {
if (e.target.checked) {
setFormData({
...formData,
analysisType: [...formData.analysisType, type],
});
} else {
setFormData({
...formData,
analysisType: formData.analysisType.filter(
(t) => t !== type
),
});
}
}}
/>
{type === 'РЫНОК' && ' Рынок'}
{type === 'КОНКУРЕНТЫ' && ' Конкуренты'}
{type === 'ЦА' && ' Целевая аудитория'}
{type === 'КАНАЛЫ' && ' Каналы'}
{type === 'SWOT' && ' SWOT'}
</label>
))}
</div>
{/* Отображение выбранных типов */}
<div>
<strong>Выбрано типов: {formData.analysisType.length}</strong>
{formData.analysisType.length > 0 && (
<div>{formData.analysisType.join(', ')}</div>
)}
</div>
</div>
{/* Остальные поля формы */}
{/* ... */}
<button type='submit'>Создать анализ</button>
</form>
);
};
export default MarketingAnalysisForm;
8. Обработка ответа API
async function handleAnalysisResponse(response: any) {
if (!response.success) {
throw new Error(response.error?.message || 'Ошибка при создании анализа');
}
const data = response.data;
// Проверяем, является ли ответ массивом
if (Array.isArray(data)) {
// Множественные анализы
console.log(`Создано ${data.length} анализов`);
// Можно показать уведомление пользователю
showNotification(
`Создано ${data.length} анализов. Они будут обработаны параллельно.`
);
// Сохраняем все ID анализов
const analysisIds = data.map(
(item: MarketingAnalysisResponse) => item.analysisId
);
// Можно перенаправить на страницу со списком анализов
navigate(`/analyses?ids=${analysisIds.join(',')}`);
// Или создать задачи для отслеживания статуса каждого анализа
data.forEach((analysis: MarketingAnalysisResponse) => {
startPollingAnalysisStatus(analysis.analysisId);
});
} else {
// Один анализ
console.log('Анализ создан:', data.analysisId);
// Перенаправление на страницу с результатами
navigate(`/analysis/${data.analysisId}`);
// Запуск отслеживания статуса
startPollingAnalysisStatus(data.analysisId);
}
}
9. Валидация на фронтенде
function validateMarketingAnalysisRequest(data: MarketingAnalysisRequest): {
isValid: boolean;
errors: string[];
} {
const errors: string[] = [];
// Валидация целевой аудитории
const hasGenders =
data.targetAudience.genders && data.targetAudience.genders.length > 0;
const hasAgeRanges =
data.targetAudience.ageRanges && data.targetAudience.ageRanges.length > 0;
const hasTypes =
data.targetAudience.types && data.targetAudience.types.length > 0;
if (!hasGenders && !hasAgeRanges && !hasTypes) {
errors.push(
'Выберите хотя бы одну опцию для целевой аудитории (гендер, возраст или тип)'
);
}
// Валидация регионов
if (!data.region || data.region.length === 0) {
errors.push('Выберите хотя бы один регион');
}
// Валидация типов анализа
if (!data.analysisType || data.analysisType.length === 0) {
errors.push('Выберите хотя бы один тип анализа');
}
// Валидация других полей
if (!data.businessNiche || data.businessNiche.trim().length < 3) {
errors.push('Ниша бизнеса должна содержать минимум 3 символа');
}
if (!data.product || data.product.trim().length < 3) {
errors.push('Продукт/услуга должна содержать минимум 3 символа');
}
if (!data.goal || data.goal.trim().length < 10) {
errors.push('Цель должна содержать минимум 10 символов');
}
return {
isValid: errors.length === 0,
errors,
};
}
10. Миграция существующего кода
Шаг 1: Обновить TypeScript интерфейсы
Замените старые интерфейсы на новые:
// Старый код
interface MarketingAnalysisRequest {
targetAudience: string;
region: string;
analysisType: string;
}
// Новый код
interface MarketingAnalysisRequest {
targetAudience: TargetAudience;
region: string[];
analysisType: string[];
}
Шаг 2: Обновить компоненты формы
- Замените
<textarea>для целевой аудитории на чекбоксы - Замените
<select>для региона на множественный выбор (чекбоксы) - Замените
<select>для типа анализа на множественный выбор (чекбоксы)
Шаг 3: Обновить обработку ответа
Добавьте проверку на массив в ответе API:
// Старый код
const analysisId = response.data.analysisId;
navigate(`/analysis/${analysisId}`);
// Новый код
const data = response.data;
if (Array.isArray(data)) {
// Обработка множественных анализов
const analysisIds = data.map((item) => item.analysisId);
navigate(`/analyses?ids=${analysisIds.join(',')}`);
} else {
// Обработка одного анализа
navigate(`/analysis/${data.analysisId}`);
}
11. Важные замечания
-
Обратная совместимость: Старый формат данных больше не поддерживается. Все запросы должны использовать новый формат.
-
Множественные анализы: При выборе нескольких типов анализа создаются отдельные записи. Каждый анализ обрабатывается независимо и имеет свой статус.
-
Валидация: Обязательно проверяйте на фронтенде, что выбрана хотя бы одна опция в каждом обязательном поле.
-
Отображение выбранных значений: Рекомендуется показывать пользователю все выбранные значения в читаемом формате.
-
UX рекомендации:
- Добавьте опцию "Выбрать все" для регионов и типов анализа
- Показывайте количество выбранных элементов
- Используйте чекбоксы вместо select для множественного выбора
-
Обработка ошибок: API вернет ошибку валидации, если:
- Не выбрана ни одна опция в целевую аудиторию
- Не выбран ни один регион
- Не выбран ни один тип анализа
- Использованы недопустимые значения
12. Примеры ошибок валидации
{
"success": false,
"message": "Ошибка валидации",
"error": {
"code": "VALIDATION_ERROR",
"message": "Ошибка валидации входных данных",
"details": {
"targetAudience": "Поле 'targetAudience' должно содержать валидную структуру с полями genders, ageRanges, types",
"region": "Поле 'region' обязательно для заполнения. Выберите хотя бы один регион",
"analysisType": "Поле 'analysisType' обязательно для заполнения. Выберите хотя бы один тип анализа"
}
}
}
13. Константы для использования
// Константы для целевой аудитории
export const VALID_GENDERS = ['Женщины', 'Мужчины'] as const;
export const VALID_AGE_RANGES = [
'20-40',
'25-45',
'18-25',
'40-60',
'60+',
] as const;
export const VALID_AUDIENCE_TYPES = [
'Семьи',
'Молодёжь',
'Все подряд',
] as const;
// Константы для типов анализа
export const VALID_ANALYSIS_TYPES = [
'РЫНОК',
'КОНКУРЕНТЫ',
'ЦА',
'КАНАЛЫ',
'SWOT',
] as const;
// Константы для регионов
export const VALID_REGIONS = [
'Алматы',
'Астана',
'Шымкент',
'Караганда',
'Актобе',
'Тараз',
'Павлодар',
'Усть-Каменогорск',
'Семей',
'Костанай',
'Кызылорда',
'Уральск',
'Петропавловск',
'Атырау',
'Актау',
'Туркестан',
'Кокшетау',
'Талдыкорган',
'Экибастуз',
'Рудный',
] as const;
// Маппинг типов анализа для отображения
export const ANALYSIS_TYPE_LABELS: Record<string, string> = {
РЫНОК: 'Рынок',
КОНКУРЕНТЫ: 'Конкуренты',
ЦА: 'Целевая аудитория',
КАНАЛЫ: 'Каналы',
SWOT: 'SWOT',
};
Заключение
Все изменения направлены на улучшение гибкости API и предоставление пользователям большего контроля над выбором параметров анализа. При правильной реализации эти изменения не должны вызвать проблем, но требуют обновления фронтенд кода.
Если у вас возникнут вопросы или проблемы при интеграции, обратитесь к команде бэкенда.