Files
app_umbrix/PRICING-LOCATION-ANALYSIS.md
Umbrix Dev a0f5d69448 📊 Полный анализ системы тарифов с выбором локаций
- PRICING-LOCATION-ANALYSIS.md (45KB): технический анализ Marzban API
- IMPLEMENTATION-SUMMARY.md (12KB): краткая выжимка для разработки
- MINI_APP_ARCHITECTURE.md: обновленная архитектура

Ключевые решения:
 Скидки за период: -10% (3м), -15% (6м), -20% (год)
 Выбор локаций из реальных нод Marzban
 Мультишаговая воронка: тариф → период → локации → оплата
 Динамическое ценообразование отклонено
⏸️ Семейные планы отложены на V2.0 (требуют Device Monitor)
 Реферальная система уже работает

MVP план: 2 недели (20 часов)
- Week 1: Backend API (8h) - /api/nodes + /api/create-user updates
- Week 2: Frontend UI (12h) - мультишаговый /app/plans/page.tsx
2026-02-08 09:16:55 +03:00

1223 lines
45 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🎯 Анализ: Система тарифов с выбором локаций + скидки за период
## 📊 Текущее состояние
### Что есть сейчас:
1.**Тарифы** определены в `/app/plans/page.tsx`:
- Trial (7 дней бесплатно)
- Start (100₽/мес, 1 локация)
- Plus (299₽/мес, 3 локации)
- Max (350₽/мес, 15+ локаций)
2.**SetupWizard** (`/components/SetupWizard.tsx`):
- Есть UI для выбора локаций
- Но используется ПОСЛЕ покупки (в wizard'е настройки)
- НЕ влияет на создание пользователя
3.**API создания пользователя** (`/app/api/create-user/route.ts`):
- Создает пользователя с фиксированными inbounds
- НЕТ привязки к нодам/локациям
- Используется hardcoded список inbounds:
```typescript
inbounds: {
vless: ['VLESS TCP', 'VLESS Reality'],
vmess: ['VMess WS'],
trojan: ['Trojan TCP']
}
```
4. ❌ **Что НЕ работает**:
- Выбор локаций НЕ сохраняется в Marzban
- Нет связи между тарифом и доступными нодами
- Нет скидок за длительные периоды (3 мес, 6 мес, год)
---
## 🔍 Анализ Marzban API
### Доступные endpoints (из https://panel.umbrix.net/docs):
#### 1. **Управление нодами** (`/api/nodes`):
```bash
GET /api/nodes # Список всех нод с их параметрами
GET /api/node/{node_id} # Детали конкретной ноды
POST /api/node # Создать ноду
PUT /api/node/{node_id} # Изменить ноду
DELETE /api/node/{node_id} # Удалить ноду
GET /api/nodes/usage # Статистика по нодам
```
**Структура ноды** (`NodeResponse`):
```json
{
"id": 1,
"name": "🇺🇸 USA Node",
"address": "194.113.210.187",
"port": 62050,
"api_port": 62051,
"status": "connected",
"message": null,
"xray_version": "1.8.21",
"usage": {
"uplink": 123456789,
"downlink": 987654321
}
}
```
#### 2. **Inbounds** (`/api/inbounds`):
```bash
GET /api/inbounds # Список всех inbounds (протоколов)
```
**Структура inbound**:
```json
{
"tag": "VLESS TCP",
"protocol": "vless",
"network": "tcp",
"tls": "reality",
"port": 443
}
```
#### 3. **Создание пользователя** (`POST /api/user`):
```json
{
"username": "test_user",
"status": "active",
"expire": 1735689600, // Unix timestamp
"data_limit": 107374182400, // Bytes
"data_limit_reset_strategy": "no_reset",
"proxies": {
"vmess": {"id": "uuid-here"},
"vless": {},
"trojan": {}
},
"inbounds": {
"vless": ["VLESS TCP", "VLESS Reality"],
"vmess": ["VMess WS"],
"trojan": ["Trojan TCP"]
},
"note": "Тариф Plus, TG: 12345"
}
```
---
## 🎨 Новая воронка с выбором локаций
### Идеальный User Flow:
```
1. Пользователь открывает /plans
2. Выбирает тариф (Start/Plus/Max)
3. Видит варианты периода оплаты:
┌─────────────────────────────────────┐
│ ⭕ 1 месяц - 299₽ (без скидки) │
│ ⭕ 3 месяца - 807₽ (-10% = 897₽) │
│ ⭕ 6 месяцев - 1523₽ (-15% = 1794₽)│
│ ⭕ 1 год - 2870₽ (-20% = 3588₽)│
└─────────────────────────────────────┘
4. Выбирает локации (в зависимости от тарифа):
┌─────────────────────────────────────┐
│ Start: 1 локация из списка │
│ Plus: 3 локации из списка │
│ Max: Все локации │
└─────────────────────────────────────┘
Список локаций (из Marzban API):
☐ 🇳🇱 Нидерланды (ping: 15ms)
☐ 🇩🇪 Германия (ping: 20ms)
☐ 🇺🇸 США (ping: 120ms)
☐ 🇸🇬 Сингапур (ping: 180ms)
☑ 🇯🇵 Япония (ping: 160ms)
☐ 🇬🇧 Великобритания (ping: 35ms)
5. Нажимает "Оплатить" → Payment gateway
6. После оплаты → API создает пользователя с выбранными локациями
```
---
## 🚀 Техническая реализация
### **Этап 1: Получить список нод из Marzban**
Создать новый API endpoint `/api/nodes`:
```typescript
// app/api/nodes/route.ts
import { NextResponse } from 'next/server';
const MARZBAN_API = process.env.MARZBAN_PANEL_URL;
const ADMIN_USERNAME = process.env.MARZBAN_ADMIN_USERNAME;
const ADMIN_PASSWORD = process.env.MARZBAN_ADMIN_PASSWORD;
export async function GET() {
try {
// 1. Получить токен админа
const tokenResponse = await fetch(`${MARZBAN_API}/api/admin/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `grant_type=password&username=${ADMIN_USERNAME}&password=${ADMIN_PASSWORD}`,
});
const { access_token } = await tokenResponse.json();
// 2. Получить список нод
const nodesResponse = await fetch(`${MARZBAN_API}/api/nodes`, {
headers: { 'Authorization': `Bearer ${access_token}` },
});
const nodes = await nodesResponse.json();
// 3. Форматировать для фронтенда
const locations = nodes
.filter(node => node.status === 'connected')
.map(node => ({
id: node.id,
name: node.name, // "🇺🇸 USA Node"
address: node.address,
ping: calculatePing(node.address), // TODO: реальный ping
country: extractCountry(node.name), // "US"
}));
return NextResponse.json({ success: true, locations });
} catch (error) {
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
}
}
function extractCountry(nodeName: string): string {
// Извлечь код страны из emoji флага
const match = nodeName.match(/🇦-🇿{2}/);
return match ? getFlagCode(match[0]) : 'Unknown';
}
function calculatePing(address: string): string {
// TODO: Реальный ping check (или хардкод по регионам)
const mockPings = {
'194.113.210.187': '120ms', // USA
'193.168.175.128': '15ms', // NL
// ...
};
return mockPings[address] || '50ms';
}
```
---
### **Этап 2: Страница выбора тарифа с периодом**
Обновить `/app/plans/page.tsx`:
```tsx
'use client';
import { useState, useEffect } from 'react';
import { Check, ChevronRight } from 'lucide-react';
type Period = '1month' | '3months' | '6months' | '1year';
type PlanType = 'start' | 'plus' | 'max';
interface Location {
id: number;
name: string;
country: string;
ping: string;
}
export default function Plans() {
const [selectedPlan, setSelectedPlan] = useState<PlanType | null>(null);
const [selectedPeriod, setSelectedPeriod] = useState<Period>('1month');
const [selectedLocations, setSelectedLocations] = useState<number[]>([]);
const [availableLocations, setAvailableLocations] = useState<Location[]>([]);
// Загрузить локации из API
useEffect(() => {
async function fetchLocations() {
const response = await fetch('/api/nodes');
const data = await response.json();
if (data.success) {
setAvailableLocations(data.locations);
}
}
fetchLocations();
}, []);
// Тарифы с ценами за разные периоды
const plans = {
start: {
name: 'Start',
locations: 1,
prices: {
'1month': 100,
'3months': 270, // -10%
'6months': 510, // -15%
'1year': 960, // -20%
}
},
plus: {
name: 'Plus',
locations: 3,
prices: {
'1month': 299,
'3months': 807, // -10%
'6months': 1523, // -15%
'1year': 2870, // -20%
}
},
max: {
name: 'Max',
locations: 999, // Все
prices: {
'1month': 350,
'3months': 945, // -10%
'6months': 1785, // -15%
'1year': 3360, // -20%
}
}
};
const handlePlanSelect = (plan: PlanType) => {
setSelectedPlan(plan);
};
const handlePeriodSelect = (period: Period) => {
setSelectedPeriod(period);
};
const handleLocationToggle = (locationId: number) => {
if (!selectedPlan) return;
const maxLocations = plans[selectedPlan].locations;
if (selectedLocations.includes(locationId)) {
setSelectedLocations(selectedLocations.filter(id => id !== locationId));
} else if (maxLocations === 999 || selectedLocations.length < maxLocations) {
setSelectedLocations([...selectedLocations, locationId]);
}
};
const handlePurchase = async () => {
if (!selectedPlan || selectedLocations.length === 0) {
alert('Выберите тариф и хотя бы одну локацию');
return;
}
// Отправить на payment gateway
const response = await fetch('/api/create-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
plan: selectedPlan,
period: selectedPeriod,
locations: selectedLocations,
telegramId: getTelegramId(),
}),
});
const data = await response.json();
if (data.success) {
window.location.href = data.paymentUrl;
}
};
const getDiscountPercent = (period: Period): number => {
const discounts = {
'1month': 0,
'3months': 10,
'6months': 15,
'1year': 20,
};
return discounts[period];
};
const calculateSavings = (plan: PlanType, period: Period): number => {
const basePrices = plans[plan].prices;
const monthlyPrice = basePrices['1month'];
const months = { '1month': 1, '3months': 3, '6months': 6, '1year': 12 }[period];
const fullPrice = monthlyPrice * months;
const discountedPrice = basePrices[period];
return fullPrice - discountedPrice;
};
return (
<div className="min-h-screen bg-slate-900 text-white p-4">
<h1 className="text-2xl font-bold mb-6">Выбор тарифа</h1>
{/* Шаг 1: Выбор тарифа */}
{!selectedPlan && (
<div className="space-y-4">
{Object.entries(plans).map(([key, plan]) => (
<button
key={key}
onClick={() => handlePlanSelect(key as PlanType)}
className="w-full p-4 bg-slate-800 rounded-lg text-left hover:bg-slate-700"
>
<h3 className="text-xl font-bold">{plan.name}</h3>
<p className="text-slate-400">
{plan.locations === 999 ? 'Все локации' : `${plan.locations} локаций`}
</p>
<p className="text-blue-400 mt-2">От {plan.prices['1month']}₽/мес</p>
</button>
))}
</div>
)}
{/* Шаг 2: Выбор периода */}
{selectedPlan && !selectedLocations.length && (
<div className="space-y-4">
<button onClick={() => setSelectedPlan(null)} className="text-blue-400">← Назад</button>
<h2 className="text-xl font-bold mb-4">Выберите период</h2>
{Object.entries(plans[selectedPlan].prices).map(([period, price]) => {
const discount = getDiscountPercent(period as Period);
const savings = calculateSavings(selectedPlan, period as Period);
return (
<button
key={period}
onClick={() => {
handlePeriodSelect(period as Period);
// Переходим к выбору локаций
}}
className="w-full p-4 bg-slate-800 rounded-lg text-left hover:bg-slate-700"
>
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-bold">
{period === '1month' && '1 месяц'}
{period === '3months' && '3 месяца'}
{period === '6months' && '6 месяцев'}
{period === '1year' && '1 год'}
</h3>
{discount > 0 && (
<p className="text-green-400 text-sm">
💰 Экономия {savings}₽ (-{discount}%)
</p>
)}
</div>
<div className="text-right">
<p className="text-2xl font-bold">{price}₽</p>
{discount > 0 && (
<p className="text-slate-400 line-through text-sm">
{plans[selectedPlan].prices['1month'] *
({ '3months': 3, '6months': 6, '1year': 12 }[period] || 1)}₽
</p>
)}
</div>
</div>
</button>
);
})}
</div>
)}
{/* Шаг 3: Выбор локаций */}
{selectedPlan && selectedPeriod && (
<div className="space-y-4">
<button onClick={() => setSelectedPeriod('1month')} className="text-blue-400">← Назад</button>
<h2 className="text-xl font-bold mb-2">Выберите локации</h2>
<p className="text-slate-400 mb-4">
{plans[selectedPlan].locations === 999
? 'Доступны все локации'
: `Выберите до ${plans[selectedPlan].locations} локаций`}
</p>
<div className="space-y-2">
{availableLocations.map(location => (
<button
key={location.id}
onClick={() => handleLocationToggle(location.id)}
disabled={
!selectedLocations.includes(location.id) &&
plans[selectedPlan].locations !== 999 &&
selectedLocations.length >= plans[selectedPlan].locations
}
className={`w-full p-4 rounded-lg border transition-colors ${
selectedLocations.includes(location.id)
? 'bg-blue-600 border-blue-400'
: 'bg-slate-800 border-slate-700 hover:border-slate-600'
}`}
>
<div className="flex justify-between items-center">
<div className="flex items-center gap-3">
{selectedLocations.includes(location.id) && (
<Check className="w-5 h-5 text-white" />
)}
<span>{location.name}</span>
</div>
<span className="text-slate-400 text-sm">{location.ping}</span>
</div>
</button>
))}
</div>
{selectedLocations.length > 0 && (
<button
onClick={handlePurchase}
className="w-full py-4 bg-blue-600 hover:bg-blue-700 rounded-lg font-bold mt-6"
>
Перейти к оплате {plans[selectedPlan].prices[selectedPeriod]}₽
<ChevronRight className="inline ml-2" />
</button>
)}
</div>
)}
</div>
);
}
function getTelegramId() {
return (window as any).Telegram?.WebApp?.initDataUnsafe?.user?.id || null;
}
```
---
### **Этап 3: Обновить API создания пользователя**
Модифицировать `/app/api/create-user/route.ts`:
```typescript
export async function POST(request: NextRequest) {
const {
planType,
period, // NEW: '1month' | '3months' | '6months' | '1year'
locationIds, // NEW: [1, 3, 5] - выбранные ID нод
telegramId,
telegramUsername,
firstName,
lastName
} = await request.json();
// 1. Получить токен админа (same as before)
// 2. Определить параметры тарифа с учетом периода
const planConfig = getPlanConfig(planType, period);
// 3. Получить список нод и их inbounds
const nodesResponse = await fetch(`${MARZBAN_API}/api/nodes`, {
headers: { 'Authorization': `Bearer ${access_token}` },
});
const allNodes = await nodesResponse.json();
// 4. Фильтровать только выбранные ноды
const selectedNodes = allNodes.filter(node => locationIds.includes(node.id));
// 5. Получить inbounds выбранных нод
const inboundsResponse = await fetch(`${MARZBAN_API}/api/inbounds`, {
headers: { 'Authorization': `Bearer ${access_token}` },
});
const allInbounds = await inboundsResponse.json();
// 6. Создать mapping inbounds для выбранных нод
const userInbounds = {
vless: [],
vmess: [],
trojan: []
};
selectedNodes.forEach(node => {
// Добавить inbounds этой ноды в конфиг пользователя
allInbounds
.filter(inbound => inbound.node_id === node.id)
.forEach(inbound => {
if (inbound.protocol === 'vless') {
userInbounds.vless.push(inbound.tag);
} else if (inbound.protocol === 'vmess') {
userInbounds.vmess.push(inbound.tag);
} else if (inbound.protocol === 'trojan') {
userInbounds.trojan.push(inbound.tag);
}
});
});
// 7. Создать пользователя с кастомными inbounds
const createUserResponse = await fetch(`${MARZBAN_API}/api/user`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${access_token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: username,
status: 'active',
expire: planConfig.expireTimestamp,
data_limit: planConfig.dataLimitBytes,
data_limit_reset_strategy: 'no_reset',
proxies: {
vmess: { id: randomUUID() },
vless: {},
trojan: {}
},
inbounds: userInbounds, // <-- ГЛАВНОЕ ИЗМЕНЕНИЕ!
note: `Тариф: ${planConfig.name}, Период: ${period}, Локации: ${selectedNodes.map(n => n.name).join(', ')}, TG: ${telegramId}`,
}),
});
// Rest of the code...
}
function getPlanConfig(planType: PlanType, period: Period) {
const basePlans = {
trial: { name: 'Trial', dataLimitGB: 10, durationDays: 7 },
start: { name: 'Start', dataLimitGB: 50, basePriceRub: 100 },
plus: { name: 'Plus', dataLimitGB: 299, basePriceRub: 299 },
max: { name: 'Max', dataLimitGB: 999999, basePriceRub: 350 },
};
const periodMultipliers = {
'1month': { months: 1, discount: 0 },
'3months': { months: 3, discount: 0.10 },
'6months': { months: 6, discount: 0.15 },
'1year': { months: 12, discount: 0.20 },
};
const plan = basePlans[planType];
const periodConfig = periodMultipliers[period];
const durationDays = periodConfig.months * 30;
const fullPrice = plan.basePriceRub * periodConfig.months;
const discountedPrice = Math.round(fullPrice * (1 - periodConfig.discount));
return {
name: plan.name,
dataLimitBytes: plan.dataLimitGB * 1024 * 1024 * 1024,
expireTimestamp: Math.floor(Date.now() / 1000) + (durationDays * 24 * 60 * 60),
priceRub: discountedPrice,
periodDays: durationDays,
};
}
```
---
### **Этап 4: Payment gateway integration**
Создать `/app/api/create-payment/route.ts`:
```typescript
// Интеграция с платежным шлюзом (Stripe / YooKassa / Coinbase)
export async function POST(request: NextRequest) {
const { plan, period, locations, telegramId } = await request.json();
const planConfig = getPlanConfig(plan, period);
// 1. Создать заказ в БД (pending payment)
const orderId = createOrder({
telegramId,
plan,
period,
locations,
amount: planConfig.priceRub,
status: 'pending',
});
// 2. Создать платежную ссылку
const paymentUrl = await createStripeCheckout({
amount: planConfig.priceRub,
currency: 'RUB',
metadata: { orderId, telegramId },
successUrl: `https://app.umbrix.net/payment-success?order=${orderId}`,
cancelUrl: `https://app.umbrix.net/plans`,
});
return NextResponse.json({ success: true, paymentUrl });
}
```
---
### **Этап 5: Webhook для обработки оплаты**
Создать `/app/api/payment-webhook/route.ts`:
```typescript
// Webhook от платежного провайдера
export async function POST(request: NextRequest) {
const payload = await request.json();
// 1. Verify signature (Stripe/YooKassa)
// 2. Проверить статус оплаты
if (payload.status === 'succeeded') {
const { orderId } = payload.metadata;
// 3. Получить данные заказа из БД
const order = getOrder(orderId);
// 4. Создать пользователя в Marzban
await fetch('/api/create-user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
planType: order.plan,
period: order.period,
locationIds: order.locations,
telegramId: order.telegramId,
}),
});
// 5. Обновить статус заказа
updateOrder(orderId, { status: 'completed' });
// 6. Отправить уведомление в Telegram
sendTelegramMessage(order.telegramId, '✅ Оплата прошла! Ваш VPN активирован.');
}
return NextResponse.json({ success: true });
}
```
---
## 🤖 Обновление n8n бота
### Добавить новые шаги в workflow:
```json
{
"nodes": [
{
"name": "Show Plans Menu",
"type": "n8n-nodes-base.telegram",
"parameters": {
"chatId": "={{ $json.userId }}",
"text": "💎 Выберите тариф:",
"replyMarkup": {
"inline_keyboard": [
[{"text": "🌍 Start (100₽/мес)", "web_app": {"url": "https://app.umbrix.net/plans?plan=start"}}],
[{"text": "🌎 Plus (299₽/мес)", "web_app": {"url": "https://app.umbrix.net/plans?plan=plus"}}],
[{"text": "🌏 Max (350₽/мес)", "web_app": {"url": "https://app.umbrix.net/plans?plan=max"}}]
]
}
}
}
]
}
```
**ВАЖНО**: Все тарифы теперь открывают Mini App с pre-selected планом → выбор периода → выбор локаций → оплата.
---
## 📋 Чеклист внедрения
### Неделя 1: Backend API
- [ ] Создать `/api/nodes` - список доступных локаций из Marzban
- [ ] Обновить `/api/create-user` - добавить параметры `period` и `locationIds`
- [ ] Добавить функцию `getPlanConfig(planType, period)` с расчетом скидок
- [ ] Реализовать mapping нод → inbounds для создания пользователя
### Неделя 2: Frontend UI
- [ ] Переработать `/app/plans/page.tsx` - мультишаговый выбор:
- Шаг 1: Тариф (Start/Plus/Max)
- Шаг 2: Период (1м/3м/6м/1год) с показом скидок
- Шаг 3: Локации (чекбоксы с ограничением по тарифу)
- [ ] Добавить компонент `PeriodSelector` с расчетом экономии
- [ ] Добавить компонент `LocationSelector` с ping и флагами стран
- [ ] Стилизация под Telegram Design System
### Неделя 3: Payment Integration
- [ ] Выбрать платежный шлюз (YooKassa для RU, Stripe для международных)
- [ ] Создать `/api/create-payment` - генерация платежных ссылок
- [ ] Создать `/api/payment-webhook` - обработка успешных оплат
- [ ] Создать БД таблицу `orders` для хранения pending платежей
- [ ] Добавить страницу `/payment-success` - редирект после оплаты
### Неделя 4: n8n Bot Update
- [ ] Добавить inline кнопки с pre-selected тарифами
- [ ] Обновить WebApp URLs с query параметрами `?plan=start`
- [ ] Добавить уведомления о успешной оплате
- [ ] Добавить команду `/renew` для продления подписки
### Неделя 5: Testing & QA
- [ ] E2E тесты полной воронки (выбор → оплата → создание пользователя)
- [ ] Тестирование на разных тарифах и периодах
- [ ] Проверка корректности создания inbounds для выбранных нод
- [ ] Load testing (100 одновременных покупок)
- [ ] Security audit payment webhook
---
## 💡 Кардинальные предложения
### **❌ Предложение 1: Динамическое ценообразование - ОТКЛОНЕНО**
~~Вместо фиксированных цен - учитывать нагрузку на ноды~~
**Причина отклонения**: Усложняет ценообразование для пользователей, все локации должны быть одинаковы по цене.
### **Предложение 2: Гибкие пакеты трафика**
Разделить тарифы на "Устройства" и "Трафик":
```
Base Plan (1 устройство):
├── 50 ГБ - 100₽/мес
├── 200 ГБ - 250₽/мес
└── ∞ ГБ - 350₽/мес
+100₽ за каждое доп. устройство
+50₽ за каждую доп. локацию
```
### **⚠️ Предложение 3: Семейные планы - ТРЕБУЕТ АНАЛИЗА**
```
Family Plan (5 устройств, безлимит):
- 499₽/мес для семьи
- Каждый член семьи выбирает свои локации
- Общий аккаунт с sub-users в Marzban
```
**Проблема**: Marzban не имеет встроенного механизма ограничения количества устройств на уровне API.
**Варианты решения**:
#### Вариант 1: Мониторинг `online_at` (рекомендуемый)
```typescript
// GET /api/user/{username} возвращает:
{
"online_at": "2026-02-08T10:30:00Z", // Последняя активность
"sub_last_user_agent": "v2rayNG/1.8.5"
}
// Логика:
// - online_at обновляется при каждом подключении
// - Если разница между текущим временем и online_at < 5 минут - устройство активно
// - Подсчитываем активные сессии через периодический poll
```
**Ограничения**:
- ❌ НЕТ прямого поля `active_connections` или `device_count`
- ❌ НЕТ поля `ip_limit` в UserCreate
- ✅ ЕСТЬ `online_at` - последняя активность
- ✅ ЕСТЬ `sub_last_user_agent` - информация об устройстве
**Реализация** (см. детали ниже в разделе "Семейные планы: Детальный анализ")
#### Вариант 2: Ограничение на уровне Xray (требует кастомных патчей)
- Модифицировать `xray_config.json` с ограничением по IP
- Требует fork Marzban с кастомной логикой
- **НЕ рекомендуется** для MVP
#### Вариант 3: IP-based limiting (приблизительный)
- Собирать статистику подключений через `/api/user/{username}/usage`
- Отслеживать уникальные IP адреса
- Ограничение: один пользователь может иметь несколько устройств с одним IP (домашняя сеть)
**Решение**: Отложить семейные планы на V2.0 до реализации мониторинга устройств.
### **✅ Предложение 4: Gamification - УЖЕ РЕАЛИЗОВАНО**
**Текущая система рефералов**:
```typescript
// /api/referral/track - отслеживание приглашений
// /api/referral/stats - статистика пользователя
Бонусы:
- +7 дней за каждого друга
- +30 дней milestone bonus за каждые 5 друзей
Формула:
bonusDays = (referralCount * 7) + (Math.floor(referralCount / 5) * 30)
Примеры:
1 друг → +7 дней
5 друзей → +35 дней (+30 milestone)
10 друзей → +70 дней (+60 milestone) = 130 дней
```
**Что можно улучшить**:
- Добавить визуальные badges (Bronze/Silver/Gold)
- Добавить leaderboard на странице `/referral`
- Уведомления в Telegram при достижении milestone
---
## 🔥 Критические улучшения воронки
### **1. A/B Testing периодов оплаты**
```typescript
// Показывать разным пользователям разные default периоды
const defaultPeriod = userId % 2 === 0 ? '3months' : '6months';
// Трекинг конверсии:
// - Какой период чаще выбирают?
// - Какая скидка конвертирует лучше?
```
### **2. Urgency триггеры**
```
⏰ Специальное предложение!
🔥 -30% на годовую подписку (осталось 2 часа)
💰 При оплате сегодня - бонус +7 дней
```
### **3. Social proof**
```
✅ Уже 1,234 пользователей выбрали тариф Plus
⭐ 4.9/5 - средняя оценка
🌍 Топ-3 локации: 🇳🇱 🇩🇪 🇺🇸
```
### **4. Упрощенная воронка для новичков**
```
Вместо 3 шагов (тариф → период → локации):
"Быстрый старт" кнопка:
→ Plus (3 месяца) + Авто-выбор 3 лучших локаций по ping
→ Сразу на оплату (экономия 807₽ вместо 897₽)
```
---
## 🎯 Priority Roadmap
### **MVP (2 недели)**:
1. ✅ API `/api/nodes` - список локаций
2. ✅ Обновить `/app/plans/page.tsx` - мультишаговый выбор
3. ✅ Обновить `/api/create-user` - кастомные inbounds
4. ⚠️ YooKassa integration (payment gateway)
### **V1.1 (4 недели)**:
5. Скидки за период (3м/6м/1год)
6. Показ экономии при выборе периода
7. n8n bot updates
### **V1.2 (6 недель)**:
8. ~~Динамическое ценообразование~~ ❌ ОТКЛОНЕНО
9. A/B testing периодов
10. Analytics dashboard (конверсия по воронке)
### **V2.0 (8 недель)**:
11. **Device Monitor Service** - мониторинг устройств через Xray логи
12. Семейные планы (после реализации п.11)
13. Gamification улучшения (badges, leaderboard)
14. Автоматическое продление подписки
---
## ❓ Вопросы для обсуждения
1. **Какой payment gateway использовать?**
- YooKassa (для РФ) ✅
- Stripe (международный) ✅
- Crypto (Coinbase Commerce) ?
2. **Какие скидки оптимальны?**
- 3 мес: -10% ✅
- 6 мес: -15% ✅
- 1 год: -20% ✅
3. **~~Нужна ли локация-специфичная цена?~~** ❌ ОТКЛОНЕНО
- Все локации одинаковы ✅
4. **Trial период - как ограничить abuse?**
- 1 trial на Telegram ID ✅
- Требовать номер телефона? ⚠️
- Требовать payment method (0₽ charge)? ⚠️
5. **Семейные планы - реализовывать в MVP?**
- ❌ Отложить на V2.0
- ⚠️ Требует Device Monitor Service (40 часов разработки)
- ✅ Для MVP - добавить в ToS ограничение без технического enforcement
---
## 📞 Следующие шаги
1. ✅ Проанализировать текущее состояние
2. ✅ Изучить Marzban API
3. ✅ Спроектировать новую воронку
4. ✅ Проанализировать ограничения устройств
5. ✅ Изучить реферальную систему
6. ⏳ **Согласовать со stakeholder'ами**
7. ⏳ **Начать разработку MVP**
**Готово к обсуждению!** 🚀
---
## 🔬 ДОПОЛНЕНИЕ: Семейные планы - Детальный анализ ограничения устройств
### Проблема:
Marzban **НЕ имеет встроенного механизма ограничения количества одновременных подключений** (devices/connections).
### Анализ Marzban API:
#### ❌ Что НЕ доступно в API:
```json
// POST /api/user - UserCreate schema НЕ имеет:
{
"ip_limit": 5, // ❌ Нет такого поля
"device_limit": 5, // ❌ Нет такого поля
"max_connections": 5, // ❌ Нет такого поля
"active_sessions": [] // ❌ Нет такого поля
}
```
#### ✅ Что ДОСТУПНО в API:
```json
// GET /api/user/{username} - UserResponse:
{
"username": "test_user",
"status": "active",
"online_at": "2026-02-08T10:30:00Z", // ✅ Последняя активность
"sub_last_user_agent": "v2rayNG/1.8.5", // ✅ User-Agent устройства
"used_traffic": 123456789,
"data_limit": 107374182400
}
// GET /api/user/{username}/usage - UsageResponse:
{
"username": "test_user",
"usages": [
{
"date": "2026-02-08",
"upload": 12345,
"download": 67890,
"total": 80235
}
]
}
```
### Варианты решения:
---
### **Вариант 1: Server-side мониторинг через `online_at` (Рекомендуемый для MVP)**
**Как работает**:
1. Периодически (каждые 30 секунд) опрашиваем `GET /api/user/{username}`
2. Проверяем `online_at` - если разница < 2 минуты → устройство активно
3. Если активных устройств > лимита → временно блокируем (`status: 'disabled'`)
**Реализация**:
```typescript
// app/api/family-plans/monitor-devices/route.ts
import { NextRequest, NextResponse } from 'next/server';
const MARZBAN_API = process.env.MARZBAN_PANEL_URL;
const ADMIN_USERNAME = process.env.MARZBAN_ADMIN_USERNAME;
const ADMIN_PASSWORD = process.env.MARZBAN_ADMIN_PASSWORD;
interface FamilyPlanUser {
username: string;
deviceLimit: number;
lastCheck: Date;
}
// В production - хранить в Redis или БД
const familyPlanUsers: Map<string, FamilyPlanUser> = new Map();
// Cron job - запускать каждые 30 секунд
export async function POST(request: NextRequest) {
try {
// 1. Получить токен админа
const tokenResponse = await fetch(`${MARZBAN_API}/api/admin/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `grant_type=password&username=${ADMIN_USERNAME}&password=${ADMIN_PASSWORD}`,
});
const { access_token } = await tokenResponse.json();
// 2. Проверить каждого пользователя Family Plan
for (const [username, config] of familyPlanUsers.entries()) {
const userResponse = await fetch(`${MARZBAN_API}/api/user/${username}`, {
headers: { 'Authorization': `Bearer ${access_token}` },
});
const user = await userResponse.json();
// 3. Проверить активность
const onlineAt = new Date(user.online_at);
const now = new Date();
const minutesSinceActive = (now.getTime() - onlineAt.getTime()) / 1000 / 60;
// Если пользователь активен (подключался менее 2 минут назад)
if (minutesSinceActive < 2) {
// ПРОБЛЕМА: Мы НЕ МОЖЕМ узнать сколько устройств подключено СЕЙЧАС!
// online_at показывает только ПОСЛЕДНЮЮ активность
// Вариант решения: собирать статистику подключений
// Но это требует доступа к логам Xray на уровне нод
}
}
return NextResponse.json({ success: true, message: 'Device monitoring completed' });
} catch (error) {
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
}
}
```
**Проблема**: `online_at` показывает только **последнюю** активность, а НЕ **количество активных сессий**.
---
### **Вариант 2: IP-based мониторинг (Приблизительный)**
**Идея**: Отслеживать уникальные IP адреса через usage API.
**Проблема**:
- Несколько устройств могут быть за одним IP (домашний роутер)
- Один телефон может менять IP (мобильный интернет)
- **НЕ точный** метод
---
### **Вариант 3: Кастомизация Xray config (Профессиональный)**
**Идея**: Модифицировать `xray_config.json` для ограничения по IP.
```json
{
"inbounds": [
{
"tag": "VLESS TCP",
"protocol": "vless",
"settings": {
"clients": [
{
"id": "uuid-here",
"email": "test_user@marzban",
"level": 0,
"maxIps": 5 // ← Кастомное поле (требует патч Xray)
}
]
}
}
]
}
```
**Проблема**:
- Требует fork Xray с кастомной логикой
- Нужно пропатчить Marzban для поддержки `maxIps`
- **Сложно** для MVP
---
### **Вариант 4: Использовать sub-users (Встроенная функция Marzban)**
**Идея**: Создать **родительский аккаунт** + **4 дочерних аккаунта**.
```typescript
// Семейный план:
// 1. Создать главного пользователя: family_john_doe
// 2. Создать 4 sub-users:
// - family_john_doe_device_1
// - family_john_doe_device_2
// - family_john_doe_device_3
// - family_john_doe_device_4
// - family_john_doe_device_5
// Каждый sub-user = отдельное устройство с собственной подпиской
```
**Преимущества**:
- ✅ Работает "из коробки" в Marzban
- ✅ Каждое устройство = отдельная подписка
- ✅ Легко контролировать (5 username'ов)
**Недостатки**:
- ❌ Пользователю нужно выдавать 5 разных subscription URLs
- ❌ Неудобно для конечного пользователя
- ❌ Не настоящий "Family Plan" (просто 5 отдельных аккаунтов)
---
### **Рекомендация для V1.0**:
#### MVP подход:
1. **Отказаться от ограничения устройств на техническом уровне**
2. **Полагаться на честность пользователей**
3. **Добавить в ToS**: "Семейный план предназначен для использования в одной семье (до 5 устройств)"
4. **Мониторить аномальный трафик**: если один аккаунт генерирует 1TB/день - вручную проверить
#### V2.0 подход (после MVP):
1. Реализовать **Server-side webhook** для мониторинга Xray логов
2. Парсить логи подключений в реальном времени:
```bash
# Xray лог показывает:
[Info] accepted tcp:xxx.xxx.xxx.xxx:12345 [email:test_user@marzban]
```
3. Собирать статистику активных IP за последние 5 минут
4. Если > deviceLimit → отправить уведомление или временно заблокировать
---
### **Архитектура для V2.0**:
```
┌─────────────────────────────────────────┐
│ Marzban Panel (API) │
│ - Создание пользователей │
│ - Управление подписками │
└────────────┬────────────────────────────┘
┌─────────────────────────────────────────┐
│ Xray Core (на каждой ноде) │
│ - Обработка трафика │
│ - Генерация логов подключений │
└────────────┬────────────────────────────┘
│ (Webhooks или log streaming)
┌─────────────────────────────────────────┐
│ Device Monitor Service (NEW!) │
│ - Парсинг логов Xray │
│ - Подсчет активных IP per user │
│ - Проверка лимитов устройств │
│ - Автоблокировка при превышении │
└────────────┬────────────────────────────┘
┌─────────────────────────────────────────┐
│ PostgreSQL / Redis │
│ - Хранение активных сессий │
│ - Хранение семейных планов │
└─────────────────────────────────────────┘
```
**Стоимость разработки**: ~40 часов (Device Monitor Service)
---
### **Итоговое решение**:
| Вариант | Точность | Сложность | Время реализации | Рекомендация |
|---------|----------|-----------|------------------|--------------|
| 1. `online_at` мониторинг | ❌ Низкая | Средняя | 8 часов | ⚠️ Не точно |
| 2. IP-based | ❌ Низкая | Низкая | 4 часа | ⚠️ Не рекомендуется |
| 3. Xray патчи | ✅ Высокая | Очень высокая | 80+ часов | ❌ Слишком сложно |
| 4. Sub-users | ✅ Высокая | Средняя | 12 часов | ⚠️ Неудобно для пользователей |
| 5. **Без ограничений + ToS** | N/A | Очень низкая | 0 часов | ✅ **Для MVP** |
| 6. Log-based monitor (V2.0) | ✅ Высокая | Высокая | 40 часов | ✅ **Для V2.0** |
**Вывод**: Для MVP - **НЕ реализовывать техническое ограничение устройств**. Добавить в ToS и мониторить аномальный трафик вручную.
Для V2.0 - разработать Device Monitor Service с парсингом Xray логов.
**Готово к обсуждению!** 🚀