# 🎯 Анализ: Система тарифов с выбором локаций + скидки за период ## 📊 Текущее состояние ### Что есть сейчас: 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(null); const [selectedPeriod, setSelectedPeriod] = useState('1month'); const [selectedLocations, setSelectedLocations] = useState([]); const [availableLocations, setAvailableLocations] = useState([]); // Загрузить локации из 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 (

Выбор тарифа

{/* Шаг 1: Выбор тарифа */} {!selectedPlan && (
{Object.entries(plans).map(([key, plan]) => ( ))}
)} {/* Шаг 2: Выбор периода */} {selectedPlan && !selectedLocations.length && (

Выберите период

{Object.entries(plans[selectedPlan].prices).map(([period, price]) => { const discount = getDiscountPercent(period as Period); const savings = calculateSavings(selectedPlan, period as Period); return ( ); })}
)} {/* Шаг 3: Выбор локаций */} {selectedPlan && selectedPeriod && (

Выберите локации

{plans[selectedPlan].locations === 999 ? 'Доступны все локации' : `Выберите до ${plans[selectedPlan].locations} локаций`}

{availableLocations.map(location => ( ))}
{selectedLocations.length > 0 && ( )}
)}
); } 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 = 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 логов. **Готово к обсуждению!** 🚀