.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -83,33 +83,69 @@ API для генерации маркетингового анализа был
|
|||||||
|
|
||||||
### 3. `targetAudience` (обязательное)
|
### 3. `targetAudience` (обязательное)
|
||||||
|
|
||||||
**Описание**: Детальное описание целевой аудитории. Это поле заменяет старое поле `client` и должно содержать более подробную информацию.
|
**Описание**: Структурированное описание целевой аудитории в формате JSON. Позволяет выбрать гендер, возрастные диапазоны и типы аудитории. Можно выбрать несколько вариантов в каждой категории.
|
||||||
|
|
||||||
**Валидация**:
|
**Валидация**:
|
||||||
|
|
||||||
- Минимальная длина: 3 символа
|
- Должно быть объектом JSON с полями `genders`, `ageRanges`, `types`
|
||||||
- Максимальная длина: 300 символов
|
- Хотя бы одно поле должно быть заполнено и содержать непустой массив
|
||||||
- Разрешены любые символы
|
- Каждое поле должно содержать только допустимые значения
|
||||||
|
|
||||||
|
**Структура**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"genders": ["Женщины", "Мужчины"],
|
||||||
|
"ageRanges": ["20-40", "25-45"],
|
||||||
|
"types": ["Семьи", "Молодёжь"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Допустимые значения**:
|
||||||
|
|
||||||
|
- `genders`: `["Женщины", "Мужчины"]` - можно выбрать один или оба
|
||||||
|
- `ageRanges`: `["20-40", "25-45", "18-25", "40-60", "60+"]` - можно выбрать один или несколько диапазонов
|
||||||
|
- `types`: `["Семьи", "Молодёжь", "Все подряд"]` - можно выбрать один или несколько типов
|
||||||
|
|
||||||
**Примеры**:
|
**Примеры**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"targetAudience": "Малый и средний бизнес, владельцы интернет-магазинов в возрасте 30-50 лет"
|
"targetAudience": {
|
||||||
"targetAudience": "Стартапы и технологические компании, нуждающиеся в быстрой разработке MVP"
|
"genders": ["Женщины"],
|
||||||
"targetAudience": "Частные лица, желающие создать личный бренд в социальных сетях"
|
"ageRanges": ["20-40"],
|
||||||
|
"types": ["Молодёжь"]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Примечание**: В отличие от старого поля `client`, которое принимало только фиксированные значения, `targetAudience` принимает свободный текст для более гибкого описания.
|
```json
|
||||||
|
"targetAudience": {
|
||||||
|
"genders": ["Женщины", "Мужчины"],
|
||||||
|
"ageRanges": ["25-45", "40-60"],
|
||||||
|
"types": ["Семьи"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
"targetAudience": {
|
||||||
|
"genders": ["Мужчины"],
|
||||||
|
"ageRanges": ["18-25"],
|
||||||
|
"types": ["Молодёжь", "Все подряд"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Примечание**: Можно выбрать несколько вариантов в каждой категории. Все выбранные значения будут отражены в анализе.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 4. `region` (обязательное)
|
### 4. `region` (обязательное)
|
||||||
|
|
||||||
**Описание**: Регион (город Казахстана), в котором работает бизнес. Это поле заменяет старое поле `location`.
|
**Описание**: Регионы (города Казахстана), в которых работает бизнес. Можно выбрать один или несколько регионов. Это поле заменяет старое поле `location`.
|
||||||
|
|
||||||
**Валидация**:
|
**Валидация**:
|
||||||
|
|
||||||
- Должно быть одним из допустимых городов Казахстана
|
- Должно быть массивом строк
|
||||||
|
- Минимум один регион должен быть выбран
|
||||||
|
- Каждый регион должен быть одним из допустимых городов Казахстана
|
||||||
- Проверка выполняется через валидатор `@ValidRegion`
|
- Проверка выполняется через валидатор `@ValidRegion`
|
||||||
|
|
||||||
**Допустимые значения** (точное совпадение):
|
**Допустимые значения** (точное совпадение):
|
||||||
@@ -138,12 +174,21 @@ API для генерации маркетингового анализа был
|
|||||||
**Примеры**:
|
**Примеры**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"region": "Алматы"
|
"region": ["Алматы"]
|
||||||
"region": "Астана"
|
|
||||||
"region": "Шымкент"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Важно**: Значение должно точно совпадать с одним из допустимых городов (регистр важен).
|
```json
|
||||||
|
"region": ["Алматы", "Астана", "Шымкент"]
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
"region": ["Астана", "Караганда"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Важно**:
|
||||||
|
- Значения должны точно совпадать с допустимыми городами (регистр важен)
|
||||||
|
- Можно выбрать все регионы, перечислив их в массиве
|
||||||
|
- Все выбранные регионы будут отражены в анализе
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -244,15 +289,41 @@ API для генерации маркетингового анализа был
|
|||||||
|
|
||||||
### 9. `analysisType` (обязательное)
|
### 9. `analysisType` (обязательное)
|
||||||
|
|
||||||
**Описание**: Тип анализа, который необходимо провести. Поле осталось без изменений.
|
**Описание**: Тип(ы) анализа, которые необходимо провести. Можно выбрать один или несколько типов анализа. При выборе нескольких типов будет создан отдельный анализ для каждого типа.
|
||||||
|
|
||||||
|
**Валидация**:
|
||||||
|
|
||||||
|
- Должно быть массивом строк
|
||||||
|
- Минимум один тип анализа должен быть выбран
|
||||||
|
- Каждый тип должен быть одним из допустимых значений
|
||||||
|
- Проверка выполняется через валидатор `@ValidAnalysisType`
|
||||||
|
|
||||||
**Допустимые значения**:
|
**Допустимые значения**:
|
||||||
|
|
||||||
- `"РЫНОК"` - Анализ рынка
|
- `"РЫНОК"` - Анализ рынка (размер рынка, динамика роста, сегменты, тренды)
|
||||||
- `"КОНКУРЕНТЫ"` - Анализ конкурентов
|
- `"КОНКУРЕНТЫ"` - Анализ конкурентов (основные конкуренты, их сильные/слабые стороны, позиционирование)
|
||||||
- `"ЦА"` - Анализ целевой аудитории
|
- `"ЦА"` - Анализ целевой аудитории (демография, психография, потребности, поведение)
|
||||||
- `"КАНАЛЫ"` - Анализ маркетинговых каналов
|
- `"КАНАЛЫ"` - Анализ маркетинговых каналов (эффективность каналов, рекомендации по выбору)
|
||||||
- `"SWOT"` - SWOT-анализ
|
- `"SWOT"` - SWOT-анализ (сильные стороны, слабые стороны, возможности, угрозы)
|
||||||
|
|
||||||
|
**Примеры**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"analysisType": ["РЫНОК"]
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
"analysisType": ["РЫНОК", "КОНКУРЕНТЫ", "ЦА"]
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
"analysisType": ["РЫНОК", "КОНКУРЕНТЫ", "ЦА", "КАНАЛЫ", "SWOT"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Важно**:
|
||||||
|
- При выборе нескольких типов анализа API создаст отдельные записи анализа для каждого типа
|
||||||
|
- В ответе будет возвращен массив с информацией о каждом созданном анализе
|
||||||
|
- Каждый анализ будет обрабатываться независимо и иметь свой статус
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -260,17 +331,61 @@ API для генерации маркетингового анализа был
|
|||||||
|
|
||||||
### POST `/api/marketing/analysis/start`
|
### POST `/api/marketing/analysis/start`
|
||||||
|
|
||||||
|
**Пример 1: Один тип анализа, один регион**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"businessNiche": "E-commerce платформы",
|
"businessNiche": "E-commerce платформы",
|
||||||
"product": "Разработка мобильных приложений для интернет-магазинов",
|
"product": "Разработка мобильных приложений для интернет-магазинов",
|
||||||
"targetAudience": "Малый и средний бизнес, владельцы интернет-магазинов в возрасте 30-50 лет, нуждающиеся в мобильных решениях",
|
"targetAudience": {
|
||||||
"region": "Алматы",
|
"genders": ["Женщины"],
|
||||||
|
"ageRanges": ["25-45"],
|
||||||
|
"types": ["Молодёжь"]
|
||||||
|
},
|
||||||
|
"region": ["Алматы"],
|
||||||
"goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев и выйти на рынок соседних регионов",
|
"goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев и выйти на рынок соседних регионов",
|
||||||
"detailLevel": "СТАНДАРТНО",
|
"detailLevel": "СТАНДАРТНО",
|
||||||
"strongSide": "Опытная команда разработчиков с 10+ летним опытом, быстрая доставка проектов, использование современных технологий",
|
"strongSide": "Опытная команда разработчиков с 10+ летним опытом, быстрая доставка проектов, использование современных технологий",
|
||||||
"weakSide": "Ограниченный маркетинговый бюджет, недостаточная узнаваемость бренда в новых регионах",
|
"weakSide": "Ограниченный маркетинговый бюджет, недостаточная узнаваемость бренда в новых регионах",
|
||||||
"analysisType": "РЫНОК"
|
"analysisType": ["РЫНОК"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример 2: Несколько типов анализа, несколько регионов**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"businessNiche": "E-commerce платформы",
|
||||||
|
"product": "Разработка мобильных приложений для интернет-магазинов",
|
||||||
|
"targetAudience": {
|
||||||
|
"genders": ["Женщины", "Мужчины"],
|
||||||
|
"ageRanges": ["25-45", "40-60"],
|
||||||
|
"types": ["Семьи", "Молодёжь"]
|
||||||
|
},
|
||||||
|
"region": ["Алматы", "Астана", "Шымкент"],
|
||||||
|
"goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев и выйти на рынок соседних регионов",
|
||||||
|
"detailLevel": "СТАНДАРТНО",
|
||||||
|
"strongSide": "Опытная команда разработчиков с 10+ летним опытом, быстрая доставка проектов",
|
||||||
|
"weakSide": "Ограниченный маркетинговый бюджет",
|
||||||
|
"analysisType": ["РЫНОК", "КОНКУРЕНТЫ", "ЦА"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример 3: Все типы анализа**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"businessNiche": "E-commerce платформы",
|
||||||
|
"product": "Разработка мобильных приложений для интернет-магазинов",
|
||||||
|
"targetAudience": {
|
||||||
|
"genders": ["Женщины", "Мужчины"],
|
||||||
|
"ageRanges": ["20-40", "25-45"],
|
||||||
|
"types": ["Семьи", "Молодёжь", "Все подряд"]
|
||||||
|
},
|
||||||
|
"region": ["Алматы", "Астана"],
|
||||||
|
"goal": "Увеличить количество клиентов на 50% за следующие 6 месяцев",
|
||||||
|
"detailLevel": "ПОДРОБНО",
|
||||||
|
"analysisType": ["РЫНОК", "КОНКУРЕНТЫ", "ЦА", "КАНАЛЫ", "SWOT"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -281,16 +396,22 @@ API для генерации маркетингового анализа был
|
|||||||
### JavaScript/TypeScript
|
### JavaScript/TypeScript
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
interface TargetAudience {
|
||||||
|
genders?: ('Женщины' | 'Мужчины')[];
|
||||||
|
ageRanges?: ('20-40' | '25-45' | '18-25' | '40-60' | '60+')[];
|
||||||
|
types?: ('Семьи' | 'Молодёжь' | 'Все подряд')[];
|
||||||
|
}
|
||||||
|
|
||||||
interface MarketingAnalysisRequest {
|
interface MarketingAnalysisRequest {
|
||||||
businessNiche: string;
|
businessNiche: string;
|
||||||
product: string;
|
product: string;
|
||||||
targetAudience: string;
|
targetAudience: TargetAudience;
|
||||||
region: string;
|
region: string[];
|
||||||
goal: string;
|
goal: string;
|
||||||
detailLevel: 'КРАТКО' | 'СТАНДАРТНО' | 'ПОДРОБНО';
|
detailLevel: 'КРАТКО' | 'СТАНДАРТНО' | 'ПОДРОБНО';
|
||||||
strongSide?: string;
|
strongSide?: string;
|
||||||
weakSide?: string;
|
weakSide?: string;
|
||||||
analysisType: 'РЫНОК' | 'КОНКУРЕНТЫ' | 'ЦА' | 'КАНАЛЫ' | 'SWOT';
|
analysisType: ('РЫНОК' | 'КОНКУРЕНТЫ' | 'ЦА' | 'КАНАЛЫ' | 'SWOT')[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Список допустимых регионов
|
// Список допустимых регионов
|
||||||
@@ -323,7 +444,7 @@ const DETAIL_LEVELS = ['КРАТКО', 'СТАНДАРТНО', 'ПОДРОБНО
|
|||||||
// Функция для отправки запроса
|
// Функция для отправки запроса
|
||||||
async function startMarketingAnalysis(
|
async function startMarketingAnalysis(
|
||||||
data: MarketingAnalysisRequest
|
data: MarketingAnalysisRequest
|
||||||
): Promise<string> {
|
): Promise<MarketingAnalysisResponse | MarketingAnalysisResponse[]> {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
'https://api.konturai.kz/api/marketing/analysis/start',
|
'https://api.konturai.kz/api/marketing/analysis/start',
|
||||||
{
|
{
|
||||||
@@ -342,21 +463,43 @@ async function startMarketingAnalysis(
|
|||||||
throw new Error(result.error?.message || 'Failed to start analysis');
|
throw new Error(result.error?.message || 'Failed to start analysis');
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.data.analysisId;
|
// Если выбрано несколько типов анализа, result.data будет массивом
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MarketingAnalysisResponse {
|
||||||
|
analysisId: string;
|
||||||
|
status: string;
|
||||||
|
estimatedCompletion: string;
|
||||||
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Пример использования
|
// Пример использования
|
||||||
const analysisId = await startMarketingAnalysis({
|
const result = await startMarketingAnalysis({
|
||||||
businessNiche: 'E-commerce платформы',
|
businessNiche: 'E-commerce платформы',
|
||||||
product: 'Разработка мобильных приложений',
|
product: 'Разработка мобильных приложений',
|
||||||
targetAudience: 'Малый и средний бизнес, владельцы интернет-магазинов',
|
targetAudience: {
|
||||||
region: 'Алматы',
|
genders: ['Женщины', 'Мужчины'],
|
||||||
|
ageRanges: ['25-45'],
|
||||||
|
types: ['Молодёжь']
|
||||||
|
},
|
||||||
|
region: ['Алматы', 'Астана'],
|
||||||
goal: 'Увеличить количество клиентов на 50% за следующие 6 месяцев',
|
goal: 'Увеличить количество клиентов на 50% за следующие 6 месяцев',
|
||||||
detailLevel: 'СТАНДАРТНО',
|
detailLevel: 'СТАНДАРТНО',
|
||||||
strongSide: 'Опытная команда, быстрая доставка',
|
strongSide: 'Опытная команда, быстрая доставка',
|
||||||
weakSide: 'Ограниченный маркетинговый бюджет',
|
weakSide: 'Ограниченный маркетинговый бюджет',
|
||||||
analysisType: 'РЫНОК',
|
analysisType: ['РЫНОК', 'КОНКУРЕНТЫ'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Если выбрано несколько типов анализа, result будет массивом
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
console.log(`Создано ${result.length} анализов`);
|
||||||
|
result.forEach((analysis, index) => {
|
||||||
|
console.log(`Анализ ${index + 1}: ${analysis.analysisId}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('Анализ создан:', result.analysisId);
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### React компонент с формой
|
### React компонент с формой
|
||||||
@@ -368,13 +511,17 @@ const MarketingAnalysisForm: React.FC = () => {
|
|||||||
const [formData, setFormData] = useState<MarketingAnalysisRequest>({
|
const [formData, setFormData] = useState<MarketingAnalysisRequest>({
|
||||||
businessNiche: '',
|
businessNiche: '',
|
||||||
product: '',
|
product: '',
|
||||||
targetAudience: '',
|
targetAudience: {
|
||||||
region: '',
|
genders: [],
|
||||||
|
ageRanges: [],
|
||||||
|
types: []
|
||||||
|
},
|
||||||
|
region: [],
|
||||||
goal: '',
|
goal: '',
|
||||||
detailLevel: 'СТАНДАРТНО',
|
detailLevel: 'СТАНДАРТНО',
|
||||||
strongSide: '',
|
strongSide: '',
|
||||||
weakSide: '',
|
weakSide: '',
|
||||||
analysisType: 'РЫНОК',
|
analysisType: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
@@ -421,31 +568,132 @@ const MarketingAnalysisForm: React.FC = () => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label>Целевая аудитория *</label>
|
<label>Целевая аудитория *</label>
|
||||||
<textarea
|
<div>
|
||||||
value={formData.targetAudience}
|
<div>
|
||||||
onChange={(e) =>
|
<label>Гендер:</label>
|
||||||
setFormData({ ...formData, targetAudience: e.target.value })
|
{['Женщины', 'Мужчины'].map((gender) => (
|
||||||
}
|
<label key={gender}>
|
||||||
required
|
<input
|
||||||
minLength={3}
|
type='checkbox'
|
||||||
maxLength={300}
|
checked={formData.targetAudience.genders?.includes(gender as any)}
|
||||||
/>
|
onChange={(e) => {
|
||||||
|
const genders = formData.targetAudience.genders || [];
|
||||||
|
if (e.target.checked) {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
targetAudience: {
|
||||||
|
...formData.targetAudience,
|
||||||
|
genders: [...genders, gender as any]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
targetAudience: {
|
||||||
|
...formData.targetAudience,
|
||||||
|
genders: genders.filter((g) => g !== gender)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{gender}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Возраст:</label>
|
||||||
|
{['20-40', '25-45', '18-25', '40-60', '60+'].map((age) => (
|
||||||
|
<label key={age}>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
checked={formData.targetAudience.ageRanges?.includes(age as any)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const ageRanges = formData.targetAudience.ageRanges || [];
|
||||||
|
if (e.target.checked) {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
targetAudience: {
|
||||||
|
...formData.targetAudience,
|
||||||
|
ageRanges: [...ageRanges, age as any]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
targetAudience: {
|
||||||
|
...formData.targetAudience,
|
||||||
|
ageRanges: ageRanges.filter((a) => a !== age)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{age}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Тип:</label>
|
||||||
|
{['Семьи', 'Молодёжь', 'Все подряд'].map((type) => (
|
||||||
|
<label key={type}>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
checked={formData.targetAudience.types?.includes(type as any)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const types = formData.targetAudience.types || [];
|
||||||
|
if (e.target.checked) {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
targetAudience: {
|
||||||
|
...formData.targetAudience,
|
||||||
|
types: [...types, type as any]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
targetAudience: {
|
||||||
|
...formData.targetAudience,
|
||||||
|
types: types.filter((t) => t !== type)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{type}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label>Регион *</label>
|
<label>Регион *</label>
|
||||||
<select
|
<div>
|
||||||
value={formData.region}
|
|
||||||
onChange={(e) => setFormData({ ...formData, region: e.target.value })}
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<option value=''>Выберите регион</option>
|
|
||||||
{VALID_REGIONS.map((region) => (
|
{VALID_REGIONS.map((region) => (
|
||||||
<option key={region} value={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}
|
{region}
|
||||||
</option>
|
</label>
|
||||||
))}
|
))}
|
||||||
</select>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -508,19 +756,34 @@ const MarketingAnalysisForm: React.FC = () => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label>Тип анализа *</label>
|
<label>Тип анализа *</label>
|
||||||
<select
|
<div>
|
||||||
value={formData.analysisType}
|
{['РЫНОК', 'КОНКУРЕНТЫ', 'ЦА', 'КАНАЛЫ', 'SWOT'].map((type) => (
|
||||||
onChange={(e) =>
|
<label key={type}>
|
||||||
setFormData({ ...formData, analysisType: e.target.value as any })
|
<input
|
||||||
}
|
type='checkbox'
|
||||||
required
|
checked={formData.analysisType.includes(type as any)}
|
||||||
>
|
onChange={(e) => {
|
||||||
<option value='РЫНОК'>Рынок</option>
|
if (e.target.checked) {
|
||||||
<option value='КОНКУРЕНТЫ'>Конкуренты</option>
|
setFormData({
|
||||||
<option value='ЦА'>Целевая аудитория</option>
|
...formData,
|
||||||
<option value='КАНАЛЫ'>Каналы</option>
|
analysisType: [...formData.analysisType, type as any]
|
||||||
<option value='SWOT'>SWOT</option>
|
});
|
||||||
</select>
|
} else {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
analysisType: formData.analysisType.filter((t) => t !== type)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{type === 'РЫНОК' && ' Рынок'}
|
||||||
|
{type === 'КОНКУРЕНТЫ' && ' Конкуренты'}
|
||||||
|
{type === 'ЦА' && ' Целевая аудитория'}
|
||||||
|
{type === 'КАНАЛЫ' && ' Каналы'}
|
||||||
|
{type === 'SWOT' && ' SWOT'}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type='submit'>Сгенерировать анализ бизнеса</button>
|
<button type='submit'>Сгенерировать анализ бизнеса</button>
|
||||||
|
|||||||
@@ -73,22 +73,41 @@ public class MarketingController {
|
|||||||
return unauthorizedResponse();
|
return unauthorizedResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create analysis record
|
// Create analysis records (one for each analysis type)
|
||||||
MarketingAnalysis analysis = marketingAnalysisService.startAnalysis(request, userId);
|
List<MarketingAnalysis> analyses = marketingAnalysisService.startAnalysis(request, userId);
|
||||||
|
|
||||||
// Start async processing
|
// Start async processing for each analysis
|
||||||
marketingAnalysisService.processAnalysis(analysis.getId(), request);
|
for (MarketingAnalysis analysis : analyses) {
|
||||||
|
marketingAnalysisService.processAnalysis(analysis.getId(), request);
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate estimated completion time (5-10 minutes)
|
// Calculate estimated completion time (5-10 minutes)
|
||||||
LocalDateTime estimatedCompletion = LocalDateTime.now().plusMinutes(8);
|
LocalDateTime estimatedCompletion = LocalDateTime.now().plusMinutes(8);
|
||||||
|
|
||||||
MarketingAnalysisResponse response = new MarketingAnalysisResponse(
|
// If multiple analyses, return list of responses
|
||||||
analysis.getId(),
|
if (analyses.size() > 1) {
|
||||||
"processing",
|
List<MarketingAnalysisResponse> responses = analyses.stream()
|
||||||
estimatedCompletion,
|
.map(analysis -> new MarketingAnalysisResponse(
|
||||||
"Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут.");
|
analysis.getId(),
|
||||||
|
"processing",
|
||||||
return ResponseEntity.ok(ApiResponse.success("Анализ запущен успешно", response));
|
estimatedCompletion,
|
||||||
|
"Анализ типа '" +
|
||||||
|
(analysis.getAnalysisType() != null && !analysis.getAnalysisType().isEmpty()
|
||||||
|
? analysis.getAnalysisType().get(0) : "неизвестно") +
|
||||||
|
"' запущен успешно. Результаты будут готовы в течение 5-10 минут."))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(
|
||||||
|
"Создано анализов: " + analyses.size() + ". Все анализы запущены успешно.",
|
||||||
|
responses));
|
||||||
|
} else {
|
||||||
|
MarketingAnalysis analysis = analyses.get(0);
|
||||||
|
MarketingAnalysisResponse response = new MarketingAnalysisResponse(
|
||||||
|
analysis.getId(),
|
||||||
|
"processing",
|
||||||
|
estimatedCompletion,
|
||||||
|
"Анализ запущен успешно. Результаты будут готовы в течение 5-10 минут.");
|
||||||
|
return ResponseEntity.ok(ApiResponse.success("Анализ запущен успешно", response));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{analysisId}")
|
@GetMapping("/{analysisId}")
|
||||||
@@ -407,13 +426,52 @@ public class MarketingController {
|
|||||||
response.setAnalysisId(analysis.getId());
|
response.setAnalysisId(analysis.getId());
|
||||||
response.setBusinessNiche(analysis.getBusinessNiche());
|
response.setBusinessNiche(analysis.getBusinessNiche());
|
||||||
response.setProduct(analysis.getProduct());
|
response.setProduct(analysis.getProduct());
|
||||||
response.setTargetAudience(analysis.getTargetAudience());
|
|
||||||
response.setRegion(analysis.getRegion());
|
// Convert targetAudience Map to String representation
|
||||||
|
Map<String, Object> targetAudienceMap = analysis.getTargetAudience();
|
||||||
|
if (targetAudienceMap != null) {
|
||||||
|
StringBuilder audienceBuilder = new StringBuilder();
|
||||||
|
if (targetAudienceMap.containsKey("genders")) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> genders = (List<String>) targetAudienceMap.get("genders");
|
||||||
|
if (genders != null && !genders.isEmpty()) {
|
||||||
|
audienceBuilder.append("Гендер: ").append(String.join(", ", genders)).append(". ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetAudienceMap.containsKey("ageRanges")) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> ageRanges = (List<String>) targetAudienceMap.get("ageRanges");
|
||||||
|
if (ageRanges != null && !ageRanges.isEmpty()) {
|
||||||
|
audienceBuilder.append("Возраст: ").append(String.join(", ", ageRanges)).append(". ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetAudienceMap.containsKey("types")) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> types = (List<String>) targetAudienceMap.get("types");
|
||||||
|
if (types != null && !types.isEmpty()) {
|
||||||
|
audienceBuilder.append("Тип: ").append(String.join(", ", types)).append(". ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response.setTargetAudience(audienceBuilder.toString().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert region List to String
|
||||||
|
List<String> regions = analysis.getRegion();
|
||||||
|
if (regions != null && !regions.isEmpty()) {
|
||||||
|
response.setRegion(String.join(", ", regions));
|
||||||
|
}
|
||||||
|
|
||||||
response.setGoal(analysis.getGoal());
|
response.setGoal(analysis.getGoal());
|
||||||
response.setDetailLevel(analysis.getDetailLevel());
|
response.setDetailLevel(analysis.getDetailLevel());
|
||||||
response.setStrongSide(analysis.getStrongSide());
|
response.setStrongSide(analysis.getStrongSide());
|
||||||
response.setWeakSide(analysis.getWeakSide());
|
response.setWeakSide(analysis.getWeakSide());
|
||||||
response.setAnalysisType(analysis.getAnalysisType());
|
|
||||||
|
// Convert analysisType List to String
|
||||||
|
List<String> analysisTypes = analysis.getAnalysisType();
|
||||||
|
if (analysisTypes != null && !analysisTypes.isEmpty()) {
|
||||||
|
response.setAnalysisType(String.join(", ", analysisTypes));
|
||||||
|
}
|
||||||
|
|
||||||
response.setStatus(analysis.getStatus());
|
response.setStatus(analysis.getStatus());
|
||||||
response.setUserId(analysis.getUserId());
|
response.setUserId(analysis.getUserId());
|
||||||
response.setCreatedAt(analysis.getCreatedAt());
|
response.setCreatedAt(analysis.getCreatedAt());
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package kz.konturai.parser.dto;
|
package kz.konturai.parser.dto;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
import jakarta.validation.constraints.Pattern;
|
import jakarta.validation.constraints.Pattern;
|
||||||
import jakarta.validation.constraints.Size;
|
import jakarta.validation.constraints.Size;
|
||||||
import kz.konturai.parser.model.AnalysisType;
|
import kz.konturai.parser.model.AnalysisType;
|
||||||
@@ -8,6 +9,10 @@ import kz.konturai.parser.model.DetailLevel;
|
|||||||
import kz.konturai.parser.validator.ValidAnalysisType;
|
import kz.konturai.parser.validator.ValidAnalysisType;
|
||||||
import kz.konturai.parser.validator.ValidDetailLevel;
|
import kz.konturai.parser.validator.ValidDetailLevel;
|
||||||
import kz.konturai.parser.validator.ValidRegion;
|
import kz.konturai.parser.validator.ValidRegion;
|
||||||
|
import kz.konturai.parser.validator.ValidTargetAudience;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class MarketingAnalysisRequest {
|
public class MarketingAnalysisRequest {
|
||||||
|
|
||||||
@@ -21,13 +26,13 @@ public class MarketingAnalysisRequest {
|
|||||||
@Pattern(regexp = "^[\\p{L}\\p{N}\\s\\-,]+$", message = "Поле 'product' содержит недопустимые символы")
|
@Pattern(regexp = "^[\\p{L}\\p{N}\\s\\-,]+$", message = "Поле 'product' содержит недопустимые символы")
|
||||||
private String product;
|
private String product;
|
||||||
|
|
||||||
@NotBlank(message = "Поле 'targetAudience' обязательно для заполнения")
|
@NotEmpty(message = "Поле 'targetAudience' обязательно для заполнения")
|
||||||
@Size(min = 3, max = 300, message = "Поле 'targetAudience' должно содержать от 3 до 300 символов")
|
@ValidTargetAudience(message = "Поле 'targetAudience' должно содержать валидную структуру с полями genders, ageRanges, types")
|
||||||
private String targetAudience;
|
private Map<String, Object> targetAudience;
|
||||||
|
|
||||||
@NotBlank(message = "Поле 'region' обязательно для заполнения")
|
@NotEmpty(message = "Поле 'region' обязательно для заполнения. Выберите хотя бы один регион")
|
||||||
@ValidRegion(message = "Поле 'region' должно быть одним из допустимых городов Казахстана")
|
@ValidRegion(message = "Поле 'region' должно содержать один или несколько допустимых городов Казахстана")
|
||||||
private String region;
|
private List<String> region;
|
||||||
|
|
||||||
@NotBlank(message = "Поле 'goal' обязательно для заполнения")
|
@NotBlank(message = "Поле 'goal' обязательно для заполнения")
|
||||||
@Size(min = 10, max = 500, message = "Поле 'goal' должно содержать от 10 до 500 символов")
|
@Size(min = 10, max = 500, message = "Поле 'goal' должно содержать от 10 до 500 символов")
|
||||||
@@ -43,15 +48,15 @@ public class MarketingAnalysisRequest {
|
|||||||
@Size(max = 500, message = "Поле 'weakSide' должно содержать не более 500 символов")
|
@Size(max = 500, message = "Поле 'weakSide' должно содержать не более 500 символов")
|
||||||
private String weakSide;
|
private String weakSide;
|
||||||
|
|
||||||
@NotBlank(message = "Поле 'analysisType' обязательно для заполнения")
|
@NotEmpty(message = "Поле 'analysisType' обязательно для заполнения. Выберите хотя бы один тип анализа")
|
||||||
@ValidAnalysisType(message = "Поле 'analysisType' должно быть одним из: РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT")
|
@ValidAnalysisType(message = "Поле 'analysisType' должно содержать один или несколько типов: РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT")
|
||||||
private String analysisType;
|
private List<String> analysisType;
|
||||||
|
|
||||||
public MarketingAnalysisRequest() {
|
public MarketingAnalysisRequest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public MarketingAnalysisRequest(String businessNiche, String product, String targetAudience, String region,
|
public MarketingAnalysisRequest(String businessNiche, String product, Map<String, Object> targetAudience, List<String> region,
|
||||||
String goal, String detailLevel, String strongSide, String weakSide, String analysisType) {
|
String goal, String detailLevel, String strongSide, String weakSide, List<String> analysisType) {
|
||||||
this.businessNiche = businessNiche;
|
this.businessNiche = businessNiche;
|
||||||
this.product = product;
|
this.product = product;
|
||||||
this.targetAudience = targetAudience;
|
this.targetAudience = targetAudience;
|
||||||
@@ -79,19 +84,19 @@ public class MarketingAnalysisRequest {
|
|||||||
this.product = product;
|
this.product = product;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getTargetAudience() {
|
public Map<String, Object> getTargetAudience() {
|
||||||
return targetAudience;
|
return targetAudience;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTargetAudience(String targetAudience) {
|
public void setTargetAudience(Map<String, Object> targetAudience) {
|
||||||
this.targetAudience = targetAudience;
|
this.targetAudience = targetAudience;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getRegion() {
|
public List<String> getRegion() {
|
||||||
return region;
|
return region;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRegion(String region) {
|
public void setRegion(List<String> region) {
|
||||||
this.region = region;
|
this.region = region;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,15 +136,21 @@ public class MarketingAnalysisRequest {
|
|||||||
this.weakSide = weakSide;
|
this.weakSide = weakSide;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAnalysisType() {
|
public List<String> getAnalysisType() {
|
||||||
return analysisType;
|
return analysisType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAnalysisType(String analysisType) {
|
public void setAnalysisType(List<String> analysisType) {
|
||||||
this.analysisType = analysisType;
|
this.analysisType = analysisType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AnalysisType getAnalysisTypeEnum() {
|
public List<AnalysisType> getAnalysisTypeEnums() {
|
||||||
return AnalysisType.fromString(analysisType);
|
if (analysisType == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return analysisType.stream()
|
||||||
|
.map(AnalysisType::fromString)
|
||||||
|
.filter(type -> type != null)
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ public class MarketingAnalysis {
|
|||||||
private String product;
|
private String product;
|
||||||
|
|
||||||
@Field("target_audience")
|
@Field("target_audience")
|
||||||
private String targetAudience;
|
private Map<String, Object> targetAudience; // JSON structure: {genders: [], ageRanges: [], types: []}
|
||||||
|
|
||||||
@Field("region")
|
@Field("region")
|
||||||
private String region;
|
private List<String> region;
|
||||||
|
|
||||||
@Field("goal")
|
@Field("goal")
|
||||||
private String goal;
|
private String goal;
|
||||||
@@ -41,7 +41,7 @@ public class MarketingAnalysis {
|
|||||||
private String weakSide;
|
private String weakSide;
|
||||||
|
|
||||||
@Field("analysis_type")
|
@Field("analysis_type")
|
||||||
private String analysisType; // РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT
|
private List<String> analysisType; // РЫНОК, КОНКУРЕНТЫ, ЦА, КАНАЛЫ, SWOT
|
||||||
|
|
||||||
@Field("status")
|
@Field("status")
|
||||||
private String status; // queued, processing, completed, failed
|
private String status; // queued, processing, completed, failed
|
||||||
@@ -73,8 +73,9 @@ public class MarketingAnalysis {
|
|||||||
this.statusHistory = new ArrayList<>();
|
this.statusHistory = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public MarketingAnalysis(String businessNiche, String product, String targetAudience, String region,
|
public MarketingAnalysis(String businessNiche, String product, Map<String, Object> targetAudience,
|
||||||
String goal, String detailLevel, String strongSide, String weakSide, String analysisType) {
|
List<String> region,
|
||||||
|
String goal, String detailLevel, String strongSide, String weakSide, List<String> analysisType) {
|
||||||
this();
|
this();
|
||||||
this.businessNiche = businessNiche;
|
this.businessNiche = businessNiche;
|
||||||
this.product = product;
|
this.product = product;
|
||||||
@@ -111,19 +112,19 @@ public class MarketingAnalysis {
|
|||||||
this.businessNiche = businessNiche;
|
this.businessNiche = businessNiche;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getTargetAudience() {
|
public Map<String, Object> getTargetAudience() {
|
||||||
return targetAudience;
|
return targetAudience;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTargetAudience(String targetAudience) {
|
public void setTargetAudience(Map<String, Object> targetAudience) {
|
||||||
this.targetAudience = targetAudience;
|
this.targetAudience = targetAudience;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getRegion() {
|
public List<String> getRegion() {
|
||||||
return region;
|
return region;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRegion(String region) {
|
public void setRegion(List<String> region) {
|
||||||
this.region = region;
|
this.region = region;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,11 +224,11 @@ public class MarketingAnalysis {
|
|||||||
this.statusHistory = statusHistory;
|
this.statusHistory = statusHistory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAnalysisType() {
|
public List<String> getAnalysisType() {
|
||||||
return analysisType;
|
return analysisType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAnalysisType(String analysisType) {
|
public void setAnalysisType(List<String> analysisType) {
|
||||||
this.analysisType = analysisType;
|
this.analysisType = analysisType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,24 +37,33 @@ public class MarketingAnalysisService {
|
|||||||
this.researchPdfService = researchPdfService;
|
this.researchPdfService = researchPdfService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MarketingAnalysis startAnalysis(MarketingAnalysisRequest request, String userId) {
|
public List<MarketingAnalysis> startAnalysis(MarketingAnalysisRequest request, String userId) {
|
||||||
MarketingAnalysis analysis = new MarketingAnalysis(
|
List<String> analysisTypes = request.getAnalysisType();
|
||||||
request.getBusinessNiche(),
|
if (analysisTypes == null || analysisTypes.isEmpty()) {
|
||||||
request.getProduct(),
|
throw new IllegalArgumentException("Необходимо выбрать хотя бы один тип анализа");
|
||||||
request.getTargetAudience(),
|
}
|
||||||
request.getRegion(),
|
|
||||||
request.getGoal(),
|
List<MarketingAnalysis> analyses = new ArrayList<>();
|
||||||
request.getDetailLevel(),
|
for (String analysisType : analysisTypes) {
|
||||||
request.getStrongSide(),
|
MarketingAnalysis analysis = new MarketingAnalysis(
|
||||||
request.getWeakSide(),
|
request.getBusinessNiche(),
|
||||||
request.getAnalysisType());
|
request.getProduct(),
|
||||||
analysis.setUserId(userId);
|
request.getTargetAudience(),
|
||||||
analysis.setStatus("queued");
|
request.getRegion(),
|
||||||
addStatusHistoryEntry(analysis, "queued", "Анализ создан и добавлен в очередь");
|
request.getGoal(),
|
||||||
analysis = repository.save(analysis);
|
request.getDetailLevel(),
|
||||||
logger.info("Marketing analysis created with ID: {} for user: {} with type: {}",
|
request.getStrongSide(),
|
||||||
analysis.getId(), userId, request.getAnalysisType());
|
request.getWeakSide(),
|
||||||
return analysis;
|
List.of(analysisType)); // Каждый анализ имеет один тип
|
||||||
|
analysis.setUserId(userId);
|
||||||
|
analysis.setStatus("queued");
|
||||||
|
addStatusHistoryEntry(analysis, "queued", "Анализ создан и добавлен в очередь");
|
||||||
|
analysis = repository.save(analysis);
|
||||||
|
analyses.add(analysis);
|
||||||
|
logger.info("Marketing analysis created with ID: {} for user: {} with type: {}",
|
||||||
|
analysis.getId(), userId, analysisType);
|
||||||
|
}
|
||||||
|
return analyses;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addStatusHistoryEntry(MarketingAnalysis analysis, String status, String message) {
|
private void addStatusHistoryEntry(MarketingAnalysis analysis, String status, String message) {
|
||||||
@@ -81,15 +90,18 @@ public class MarketingAnalysisService {
|
|||||||
|
|
||||||
logger.info("Starting marketing analysis processing for ID: {}", analysisId);
|
logger.info("Starting marketing analysis processing for ID: {}", analysisId);
|
||||||
|
|
||||||
|
// Create a modified request with single analysis type for this analysis
|
||||||
|
MarketingAnalysisRequest singleTypeRequest = createSingleTypeRequest(request, analysis.getAnalysisType().get(0));
|
||||||
|
|
||||||
// Generate marketing report
|
// Generate marketing report
|
||||||
Map<String, Object> reportData = generateMarketingReport(request);
|
Map<String, Object> reportData = generateMarketingReport(singleTypeRequest);
|
||||||
|
|
||||||
// Generate markdown content for PDF
|
// Generate markdown content for PDF
|
||||||
String markdownContent = buildMarkdownFromReport(reportData, request);
|
String markdownContent = buildMarkdownFromReport(reportData, singleTypeRequest);
|
||||||
|
|
||||||
// Generate PDF
|
// Generate PDF
|
||||||
byte[] pdfBytes = researchPdfService.generatePdfReport(
|
byte[] pdfBytes = researchPdfService.generatePdfReport(
|
||||||
"Маркетинговый анализ: " + request.getProduct(),
|
"Маркетинговый анализ: " + singleTypeRequest.getProduct(),
|
||||||
markdownContent,
|
markdownContent,
|
||||||
new ArrayList<>());
|
new ArrayList<>());
|
||||||
|
|
||||||
@@ -124,11 +136,26 @@ public class MarketingAnalysisService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private MarketingAnalysisRequest createSingleTypeRequest(MarketingAnalysisRequest originalRequest, String analysisType) {
|
||||||
|
MarketingAnalysisRequest singleTypeRequest = new MarketingAnalysisRequest();
|
||||||
|
singleTypeRequest.setBusinessNiche(originalRequest.getBusinessNiche());
|
||||||
|
singleTypeRequest.setProduct(originalRequest.getProduct());
|
||||||
|
singleTypeRequest.setTargetAudience(originalRequest.getTargetAudience());
|
||||||
|
singleTypeRequest.setRegion(originalRequest.getRegion());
|
||||||
|
singleTypeRequest.setGoal(originalRequest.getGoal());
|
||||||
|
singleTypeRequest.setDetailLevel(originalRequest.getDetailLevel());
|
||||||
|
singleTypeRequest.setStrongSide(originalRequest.getStrongSide());
|
||||||
|
singleTypeRequest.setWeakSide(originalRequest.getWeakSide());
|
||||||
|
singleTypeRequest.setAnalysisType(List.of(analysisType));
|
||||||
|
return singleTypeRequest;
|
||||||
|
}
|
||||||
|
|
||||||
private Map<String, Object> generateMarketingReport(MarketingAnalysisRequest request) {
|
private Map<String, Object> generateMarketingReport(MarketingAnalysisRequest request) {
|
||||||
Map<String, Object> report = new HashMap<>();
|
Map<String, Object> report = new HashMap<>();
|
||||||
|
|
||||||
// Get analysis type and detail level
|
// Get analysis type and detail level - теперь всегда один тип
|
||||||
AnalysisType analysisType = request.getAnalysisTypeEnum();
|
List<AnalysisType> analysisTypes = request.getAnalysisTypeEnums();
|
||||||
|
AnalysisType analysisType = analysisTypes != null && !analysisTypes.isEmpty() ? analysisTypes.get(0) : null;
|
||||||
String analysisTypeName = analysisType != null ? analysisType.getDisplayName() : "Общий";
|
String analysisTypeName = analysisType != null ? analysisType.getDisplayName() : "Общий";
|
||||||
DetailLevel detailLevel = request.getDetailLevelEnum();
|
DetailLevel detailLevel = request.getDetailLevelEnum();
|
||||||
String detailLevelName = detailLevel != null ? detailLevel.getDisplayName() : "Стандартно";
|
String detailLevelName = detailLevel != null ? detailLevel.getDisplayName() : "Стандартно";
|
||||||
@@ -137,8 +164,40 @@ public class MarketingAnalysisService {
|
|||||||
StringBuilder contextBuilder = new StringBuilder();
|
StringBuilder contextBuilder = new StringBuilder();
|
||||||
contextBuilder.append("Ниша бизнеса: ").append(request.getBusinessNiche()).append("\n");
|
contextBuilder.append("Ниша бизнеса: ").append(request.getBusinessNiche()).append("\n");
|
||||||
contextBuilder.append("Продукт/услуга: ").append(request.getProduct()).append("\n");
|
contextBuilder.append("Продукт/услуга: ").append(request.getProduct()).append("\n");
|
||||||
contextBuilder.append("Целевая аудитория: ").append(request.getTargetAudience()).append("\n");
|
|
||||||
contextBuilder.append("Регион: ").append(request.getRegion()).append("\n");
|
// Format target audience
|
||||||
|
Map<String, Object> targetAudienceData = request.getTargetAudience();
|
||||||
|
if (targetAudienceData != null) {
|
||||||
|
StringBuilder audienceBuilder = new StringBuilder();
|
||||||
|
if (targetAudienceData.containsKey("genders")) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> genders = (List<String>) targetAudienceData.get("genders");
|
||||||
|
if (genders != null && !genders.isEmpty()) {
|
||||||
|
audienceBuilder.append("Гендер: ").append(String.join(", ", genders)).append(". ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetAudienceData.containsKey("ageRanges")) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> ageRanges = (List<String>) targetAudienceData.get("ageRanges");
|
||||||
|
if (ageRanges != null && !ageRanges.isEmpty()) {
|
||||||
|
audienceBuilder.append("Возраст: ").append(String.join(", ", ageRanges)).append(". ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetAudienceData.containsKey("types")) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> types = (List<String>) targetAudienceData.get("types");
|
||||||
|
if (types != null && !types.isEmpty()) {
|
||||||
|
audienceBuilder.append("Тип: ").append(String.join(", ", types)).append(". ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contextBuilder.append("Целевая аудитория: ").append(audienceBuilder.toString().trim()).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format regions
|
||||||
|
List<String> regions = request.getRegion();
|
||||||
|
if (regions != null && !regions.isEmpty()) {
|
||||||
|
contextBuilder.append("Регион: ").append(String.join(", ", regions)).append("\n");
|
||||||
|
}
|
||||||
contextBuilder.append("Цель на 6-12 месяцев: ").append(request.getGoal()).append("\n");
|
contextBuilder.append("Цель на 6-12 месяцев: ").append(request.getGoal()).append("\n");
|
||||||
contextBuilder.append("Уровень детализации: ").append(detailLevelName).append("\n");
|
contextBuilder.append("Уровень детализации: ").append(detailLevelName).append("\n");
|
||||||
if (request.getStrongSide() != null && !request.getStrongSide().trim().isEmpty()) {
|
if (request.getStrongSide() != null && !request.getStrongSide().trim().isEmpty()) {
|
||||||
@@ -395,8 +454,41 @@ public class MarketingAnalysisService {
|
|||||||
markdown.append("## Информация о бизнесе\n\n");
|
markdown.append("## Информация о бизнесе\n\n");
|
||||||
markdown.append("- **Ниша бизнеса:** ").append(request.getBusinessNiche()).append("\n");
|
markdown.append("- **Ниша бизнеса:** ").append(request.getBusinessNiche()).append("\n");
|
||||||
markdown.append("- **Продукт/услуга:** ").append(request.getProduct()).append("\n");
|
markdown.append("- **Продукт/услуга:** ").append(request.getProduct()).append("\n");
|
||||||
markdown.append("- **Целевая аудитория:** ").append(request.getTargetAudience()).append("\n");
|
|
||||||
markdown.append("- **Регион:** ").append(request.getRegion()).append("\n");
|
// Format target audience
|
||||||
|
Map<String, Object> targetAudienceData = request.getTargetAudience();
|
||||||
|
if (targetAudienceData != null) {
|
||||||
|
StringBuilder audienceBuilder = new StringBuilder();
|
||||||
|
if (targetAudienceData.containsKey("genders")) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> genders = (List<String>) targetAudienceData.get("genders");
|
||||||
|
if (genders != null && !genders.isEmpty()) {
|
||||||
|
audienceBuilder.append("Гендер: ").append(String.join(", ", genders)).append(". ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetAudienceData.containsKey("ageRanges")) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> ageRanges = (List<String>) targetAudienceData.get("ageRanges");
|
||||||
|
if (ageRanges != null && !ageRanges.isEmpty()) {
|
||||||
|
audienceBuilder.append("Возраст: ").append(String.join(", ", ageRanges)).append(". ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetAudienceData.containsKey("types")) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> types = (List<String>) targetAudienceData.get("types");
|
||||||
|
if (types != null && !types.isEmpty()) {
|
||||||
|
audienceBuilder.append("Тип: ").append(String.join(", ", types)).append(". ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
markdown.append("- **Целевая аудитория:** ").append(audienceBuilder.toString().trim()).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format regions
|
||||||
|
List<String> regions = request.getRegion();
|
||||||
|
if (regions != null && !regions.isEmpty()) {
|
||||||
|
markdown.append("- **Регион:** ").append(String.join(", ", regions)).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
markdown.append("- **Цель на 6-12 месяцев:** ").append(request.getGoal()).append("\n");
|
markdown.append("- **Цель на 6-12 месяцев:** ").append(request.getGoal()).append("\n");
|
||||||
|
|
||||||
String detailLevelName = (String) reportData.get("detailLevel");
|
String detailLevelName = (String) reportData.get("detailLevel");
|
||||||
|
|||||||
@@ -3,18 +3,25 @@ package kz.konturai.parser.validator;
|
|||||||
import jakarta.validation.ConstraintValidator;
|
import jakarta.validation.ConstraintValidator;
|
||||||
import jakarta.validation.ConstraintValidatorContext;
|
import jakarta.validation.ConstraintValidatorContext;
|
||||||
import kz.konturai.parser.model.AnalysisType;
|
import kz.konturai.parser.model.AnalysisType;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class AnalysisTypeValidator implements ConstraintValidator<ValidAnalysisType, String> {
|
public class AnalysisTypeValidator implements ConstraintValidator<ValidAnalysisType, List<String>> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(ValidAnalysisType constraintAnnotation) {
|
public void initialize(ValidAnalysisType constraintAnnotation) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isValid(String value, ConstraintValidatorContext context) {
|
public boolean isValid(List<String> value, ConstraintValidatorContext context) {
|
||||||
if (value == null || value.trim().isEmpty()) {
|
if (value == null || value.isEmpty()) {
|
||||||
return true; // @NotBlank will handle null/empty
|
return true; // @NotEmpty will handle null/empty
|
||||||
}
|
}
|
||||||
return AnalysisType.fromString(value) != null;
|
// Проверяем, что все типы анализа в списке валидны
|
||||||
|
for (String type : value) {
|
||||||
|
if (type == null || type.trim().isEmpty() || AnalysisType.fromString(type) == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import jakarta.validation.ConstraintValidatorContext;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class RegionValidator implements ConstraintValidator<ValidRegion, String> {
|
public class RegionValidator implements ConstraintValidator<ValidRegion, List<String>> {
|
||||||
|
|
||||||
private static final List<String> VALID_REGIONS = Arrays.asList(
|
private static final List<String> VALID_REGIONS = Arrays.asList(
|
||||||
"Алматы",
|
"Алматы",
|
||||||
@@ -34,11 +34,17 @@ public class RegionValidator implements ConstraintValidator<ValidRegion, String>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isValid(String value, ConstraintValidatorContext context) {
|
public boolean isValid(List<String> value, ConstraintValidatorContext context) {
|
||||||
if (value == null || value.trim().isEmpty()) {
|
if (value == null || value.isEmpty()) {
|
||||||
return true; // @NotBlank will handle null/empty
|
return true; // @NotEmpty will handle null/empty
|
||||||
}
|
}
|
||||||
return VALID_REGIONS.contains(value);
|
// Проверяем, что все регионы в списке валидны
|
||||||
|
for (String region : value) {
|
||||||
|
if (region == null || region.trim().isEmpty() || !VALID_REGIONS.contains(region)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package kz.konturai.parser.validator;
|
||||||
|
|
||||||
|
import jakarta.validation.ConstraintValidator;
|
||||||
|
import jakarta.validation.ConstraintValidatorContext;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class TargetAudienceValidator implements ConstraintValidator<ValidTargetAudience, Map<String, Object>> {
|
||||||
|
|
||||||
|
private static final List<String> VALID_GENDERS = List.of("Женщины", "Мужчины");
|
||||||
|
private static final List<String> VALID_AGE_RANGES = List.of("20-40", "25-45", "18-25", "40-60", "60+");
|
||||||
|
private static final List<String> VALID_TYPES = List.of("Семьи", "Молодёжь", "Все подряд");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize(ValidTargetAudience constraintAnnotation) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValid(Map<String, Object> value, ConstraintValidatorContext context) {
|
||||||
|
if (value == null || value.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем наличие хотя бы одного поля
|
||||||
|
boolean hasGenders = value.containsKey("genders") && value.get("genders") != null;
|
||||||
|
boolean hasAgeRanges = value.containsKey("ageRanges") && value.get("ageRanges") != null;
|
||||||
|
boolean hasTypes = value.containsKey("types") && value.get("types") != null;
|
||||||
|
|
||||||
|
if (!hasGenders && !hasAgeRanges && !hasTypes) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Валидация genders
|
||||||
|
if (hasGenders) {
|
||||||
|
Object gendersObj = value.get("genders");
|
||||||
|
if (!(gendersObj instanceof List)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> genders = (List<String>) gendersObj;
|
||||||
|
if (genders.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (String gender : genders) {
|
||||||
|
if (!VALID_GENDERS.contains(gender)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Валидация ageRanges
|
||||||
|
if (hasAgeRanges) {
|
||||||
|
Object ageRangesObj = value.get("ageRanges");
|
||||||
|
if (!(ageRangesObj instanceof List)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> ageRanges = (List<String>) ageRangesObj;
|
||||||
|
if (ageRanges.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (String ageRange : ageRanges) {
|
||||||
|
if (!VALID_AGE_RANGES.contains(ageRange)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Валидация types
|
||||||
|
if (hasTypes) {
|
||||||
|
Object typesObj = value.get("types");
|
||||||
|
if (!(typesObj instanceof List)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> types = (List<String>) typesObj;
|
||||||
|
if (types.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (String type : types) {
|
||||||
|
if (!VALID_TYPES.contains(type)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ import java.lang.annotation.Target;
|
|||||||
@Target({ ElementType.FIELD, ElementType.PARAMETER })
|
@Target({ ElementType.FIELD, ElementType.PARAMETER })
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
public @interface ValidRegion {
|
public @interface ValidRegion {
|
||||||
String message() default "Поле 'region' должно быть одним из допустимых городов Казахстана";
|
String message() default "Поле 'region' должно содержать один или несколько допустимых городов Казахстана";
|
||||||
|
|
||||||
Class<?>[] groups() default {};
|
Class<?>[] groups() default {};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package kz.konturai.parser.validator;
|
||||||
|
|
||||||
|
import jakarta.validation.Constraint;
|
||||||
|
import jakarta.validation.Payload;
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
@Documented
|
||||||
|
@Constraint(validatedBy = TargetAudienceValidator.class)
|
||||||
|
@Target({ ElementType.FIELD, ElementType.PARAMETER })
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface ValidTargetAudience {
|
||||||
|
String message() default "Поле 'targetAudience' должно содержать валидную структуру с полями genders, ageRanges, types";
|
||||||
|
|
||||||
|
Class<?>[] groups() default {};
|
||||||
|
|
||||||
|
Class<? extends Payload>[] payload() default {};
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user