From b43eb3c724d734909478ac3112f3cc296a00fce1 Mon Sep 17 00:00:00 2001 From: Umbrix Dev Date: Fri, 6 Feb 2026 20:51:40 +0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=D0=A0=D0=B5=D1=84=D0=B5=D1=80?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D1=81=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D0=B5=D0=BC=D0=B0:=20=D0=91=D0=94,=20API,=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **База данных:** - Создана таблица referrals в MariaDB (193.168.175.128) - Поля: username, referrer_username, referral_count, bonus_days_earned - Foreign keys к users таблице с CASCADE/SET NULL **API Endpoints:** - POST /api/referral/track - отслеживание новых рефералов - Автоматический расчёт бонусов: +7 дней за каждого - Milestone bonus: +30 дней за каждые 5 рефералов - Обновление expire даты реферера в users таблице - GET /api/referral/stats?username=xxx - статистика - Возвращает количество рефералов, бонусные дни - Список приглашённых пользователей со статусами **Интеграция:** - POST /api/create-user принимает referrerId параметр - Автоматический вызов /api/referral/track после создания юзера - Параметр ref из URL при активации trial **UI:** - /app/referral/page.tsx - страница статистики - 3 KPI карточки: рефералов, бонусных дней, milestone - Реферальная ссылка с кнопкой копирования - Список приглашённых юзеров с иконками статуса - Инфоблок о механике начисления бонусов **ReferralModal обновлён:** - Добавлена кнопка «Моя статистика» → /referral - Перенос Share/Copy кнопок на второй/третий план **Зависимости:** - mysql2@3.16.3 - для подключения к MariaDB **Логика бонусов:** - +7 дней за каждого успешного реферала - +30 дней бонус за каждые 5 рефералов (milestone) - Автоматическое обновление expire поля в users таблице - Сохранение всех бонусов в bonus_days_earned --- app/api/create-user/route.ts | 22 ++- app/api/referral/stats/route.ts | 90 ++++++++++ app/api/referral/track/route.ts | 135 +++++++++++++++ app/page.tsx | 5 + app/referral/page.tsx | 296 ++++++++++++++++++++++++++++++++ components/ReferralModal.tsx | 17 +- package-lock.json | 123 +++++++++++++ package.json | 1 + 8 files changed, 684 insertions(+), 5 deletions(-) create mode 100644 app/api/referral/stats/route.ts create mode 100644 app/api/referral/track/route.ts create mode 100644 app/referral/page.tsx diff --git a/app/api/create-user/route.ts b/app/api/create-user/route.ts index efbf797..ebd74d8 100644 --- a/app/api/create-user/route.ts +++ b/app/api/create-user/route.ts @@ -19,9 +19,9 @@ if (!ADMIN_USERNAME || !ADMIN_PASSWORD) { export async function POST(request: NextRequest) { try { - const { planType, telegramId, telegramUsername, firstName, lastName } = await request.json(); + const { planType, telegramId, telegramUsername, firstName, lastName, referrerId } = await request.json(); - logger.debug('📥 Received data:', { planType, telegramId, telegramUsername, firstName, lastName }); + logger.debug('📥 Received data:', { planType, telegramId, telegramUsername, firstName, lastName, referrerId }); // 1. Получаем токен админа const tokenResponse = await fetch(`${MARZBAN_API}/api/admin/token`, { @@ -101,6 +101,24 @@ export async function POST(request: NextRequest) { // 5. Получаем subscription token const subscriptionToken = userData.subscription_url?.split('/sub/')[1]?.replace(/\/$/, '') || username; + // 6. Track referral if referrerId provided + if (referrerId) { + try { + await fetch(`${request.nextUrl.origin}/api/referral/track`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + username: username, + referrer_username: referrerId, + }), + }); + logger.debug('✅ Referral tracked:', { username, referrer: referrerId }); + } catch (refError) { + logger.error('❌ Referral tracking failed:', refError); + // Don't fail user creation if referral tracking fails + } + } + return NextResponse.json({ success: true, token: subscriptionToken, diff --git a/app/api/referral/stats/route.ts b/app/api/referral/stats/route.ts new file mode 100644 index 0000000..69fa321 --- /dev/null +++ b/app/api/referral/stats/route.ts @@ -0,0 +1,90 @@ +// API endpoint для получения статистики реферальной программы +// GET /api/referral/stats?username=xxx - возвращает статистику пользователя + +import { NextRequest, NextResponse } from 'next/server'; +import mysql from 'mysql2/promise'; + +// Database connection config +const dbConfig = { + host: '193.168.175.128', + user: 'marzban_user', + password: '2CuopqFd0Y5V5n/qBM+eygOQb6aC8B8pACcdHjeVJsE=', + database: 'marzban_prod', +}; + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const username = searchParams.get('username'); + + if (!username) { + return NextResponse.json( + { success: false, error: 'Username is required' }, + { status: 400 } + ); + } + + // Connect to database + const connection = await mysql.createConnection(dbConfig); + + try { + // Get user's referral stats + const [statsRows] = await connection.query( + 'SELECT * FROM referrals WHERE username = ?', + [username] + ); + + if ((statsRows as any[]).length === 0) { + return NextResponse.json({ + success: true, + hasReferrals: false, + stats: { + referral_count: 0, + bonus_days_earned: 0, + total_referrals_used: 0, + }, + }); + } + + const stats = (statsRows as any[])[0]; + + // Get list of referred users + const [referredRows] = await connection.query( + `SELECT u.username, u.created_at, u.status + FROM referrals r + JOIN users u ON r.username = u.username + WHERE r.referrer_username = ? + ORDER BY r.created_at DESC`, + [username] + ); + + return NextResponse.json({ + success: true, + hasReferrals: true, + stats: { + referral_count: stats.referral_count || 0, + bonus_days_earned: stats.bonus_days_earned || 0, + total_referrals_used: stats.total_referrals_used || 0, + created_at: stats.created_at, + updated_at: stats.updated_at, + }, + referred_users: (referredRows as any[]).map((user: any) => ({ + username: user.username, + created_at: user.created_at, + status: user.status, + })), + }); + } finally { + await connection.end(); + } + } catch (error) { + console.error('Referral stats error:', error); + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 } + ); + } +} diff --git a/app/api/referral/track/route.ts b/app/api/referral/track/route.ts new file mode 100644 index 0000000..02030af --- /dev/null +++ b/app/api/referral/track/route.ts @@ -0,0 +1,135 @@ +// API endpoint для отслеживания реферальной регистрации +// POST /api/referral/track - записывает нового пользователя с реферером + +import { NextRequest, NextResponse } from 'next/server'; +import mysql from 'mysql2/promise'; + +// Database connection config +const dbConfig = { + host: '193.168.175.128', + user: 'marzban_user', + password: '2CuopqFd0Y5V5n/qBM+eygOQb6aC8B8pACcdHjeVJsE=', + database: 'marzban_prod', +}; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { username, referrer_username } = body; + + if (!username) { + return NextResponse.json( + { success: false, error: 'Username is required' }, + { status: 400 } + ); + } + + // Connect to database + const connection = await mysql.createConnection(dbConfig); + + try { + // Check if user already exists in referrals table + const [existingRows] = await connection.query( + 'SELECT id FROM referrals WHERE username = ?', + [username] + ); + + if ((existingRows as any[]).length > 0) { + return NextResponse.json({ + success: false, + error: 'User already tracked', + }); + } + + // Insert new referral record + await connection.query( + 'INSERT INTO referrals (username, referrer_username) VALUES (?, ?)', + [username, referrer_username || null] + ); + + // If referrer exists, increment their referral_count + if (referrer_username) { + // Check if referrer exists in users table + const [referrerRows] = await connection.query( + 'SELECT username FROM users WHERE username = ?', + [referrer_username] + ); + + if ((referrerRows as any[]).length > 0) { + // Update or insert referrer's stats + await connection.query( + `INSERT INTO referrals (username, referral_count) + VALUES (?, 1) + ON DUPLICATE KEY UPDATE + referral_count = referral_count + 1, + updated_at = CURRENT_TIMESTAMP`, + [referrer_username] + ); + + // Calculate bonus days + const [statsRows] = await connection.query( + 'SELECT referral_count FROM referrals WHERE username = ?', + [referrer_username] + ); + + const referralCount = (statsRows as any[])[0]?.referral_count || 0; + let bonusDays = 0; + + // +7 days for each referral + bonusDays = referralCount * 7; + + // +30 days milestone bonus for every 5 referrals + if (referralCount >= 5) { + const milestones = Math.floor(referralCount / 5); + bonusDays += milestones * 30; + } + + // Update bonus_days_earned + await connection.query( + 'UPDATE referrals SET bonus_days_earned = ? WHERE username = ?', + [bonusDays, referrer_username] + ); + + // Add bonus days to referrer's expire date in users table + await connection.query( + `UPDATE users + SET expire = CASE + WHEN expire IS NULL OR expire < UNIX_TIMESTAMP() + THEN UNIX_TIMESTAMP() + (? * 86400) + ELSE expire + (7 * 86400) + END + WHERE username = ?`, + [bonusDays, referrer_username] + ); + + return NextResponse.json({ + success: true, + message: 'Referral tracked successfully', + referrer_bonus: { + username: referrer_username, + new_referral_count: referralCount, + bonus_days_added: 7, + total_bonus_days: bonusDays, + }, + }); + } + } + + return NextResponse.json({ + success: true, + message: 'User tracked without referrer', + }); + } finally { + await connection.end(); + } + } catch (error) { + console.error('Referral track error:', error); + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 } + ); + } +} diff --git a/app/page.tsx b/app/page.tsx index 51bb85e..8f134c7 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -175,6 +175,10 @@ export default function Home() { const handleActivateTrial = async () => { setIsLoading(true); try { + // Получаем referrerId из URL (если есть) + const urlParams = new URLSearchParams(window.location.search); + const referrerId = urlParams.get('ref'); + // Создаем trial подписку через API const response = await fetch('/api/create-user', { method: 'POST', @@ -184,6 +188,7 @@ export default function Home() { telegramId: Date.now(), // Временно, пока нет настоящего Telegram ID telegramUsername: 'demo_user', firstName: 'Demo', + referrerId: referrerId || undefined, }), }); diff --git a/app/referral/page.tsx b/app/referral/page.tsx new file mode 100644 index 0000000..0651254 --- /dev/null +++ b/app/referral/page.tsx @@ -0,0 +1,296 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { + Shield, + ArrowLeft, + Users, + Gift, + TrendingUp, + Calendar, + CheckCircle, + XCircle, + Clock, + Copy, + Share2 +} from 'lucide-react'; + +interface ReferralStats { + referral_count: number; + bonus_days_earned: number; + total_referrals_used: number; + created_at: string; + updated_at: string; +} + +interface ReferredUser { + username: string; + created_at: string; + status: string; +} + +export default function ReferralPage() { + const router = useRouter(); + const [stats, setStats] = useState(null); + const [referredUsers, setReferredUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [username, setUsername] = useState(''); + const [referralUrl, setReferralUrl] = useState(''); + const [showToast, setShowToast] = useState(false); + const [toastMessage, setToastMessage] = useState(''); + + useEffect(() => { + const loadReferralStats = async () => { + // Get username from localStorage + const token = localStorage.getItem('subscriptionToken'); + if (!token) { + router.push('/'); + return; + } + + const userId = token.split('_')[0]; + setUsername(userId); + + // Generate referral URL + const botUsername = process.env.NEXT_PUBLIC_TELEGRAM_BOT_USERNAME || 'Dorod_vps_bot'; + const url = `https://t.me/${botUsername}?start=ref_${userId}`; + setReferralUrl(url); + + // Fetch stats from API + try { + const response = await fetch(`/api/referral/stats?username=${userId}`); + const data = await response.json(); + + if (data.success && data.hasReferrals) { + setStats(data.stats); + setReferredUsers(data.referred_users || []); + } + } catch (error) { + console.error('Failed to load referral stats:', error); + } finally { + setLoading(false); + } + }; + + loadReferralStats(); + }, [router]); + + const copyToClipboard = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + showToastNotification('✅ Скопировано в буфер обмена!'); + } catch (err) { + console.error('Failed to copy:', err); + showToastNotification('❌ Ошибка копирования'); + } + }; + + const showToastNotification = (message: string) => { + setToastMessage(message); + setShowToast(true); + setTimeout(() => setShowToast(false), 3000); + }; + + const shareReferralLink = async () => { + const shareText = `🚀 Попробуй Umbrix VPN - быстрый и безопасный VPN!\n\n✨ Получи 7 дней бесплатно по моей ссылке:\n${referralUrl}`; + + if (navigator.share) { + try { + await navigator.share({ + title: 'Umbrix VPN - Пригласи друга!', + text: shareText, + url: referralUrl, + }); + } catch (err) { + if ((err as Error).name !== 'AbortError') { + console.error('Share failed:', err); + copyToClipboard(shareText); + } + } + } else { + copyToClipboard(shareText); + } + }; + + const getStatusIcon = (status: string) => { + switch (status) { + case 'active': + return ; + case 'expired': + return ; + case 'disabled': + return ; + default: + return ; + } + }; + + const getStatusText = (status: string) => { + switch (status) { + case 'active': + return 'Активен'; + case 'expired': + return 'Истёк'; + case 'disabled': + return 'Отключён'; + default: + return status; + } + }; + + if (loading) { + return ( +
+
+
+

Загрузка...

+
+
+ ); + } + + return ( +
+ {/* Header */} +
+ +
+ + Реферальная программа +
+
{/* Spacer */} +
+ + {/* Main Content */} +
+ {/* Stats Cards */} +
+
+ +
{stats?.referral_count || 0}
+
Рефералов
+
+ +
+ +
{stats?.bonus_days_earned || 0}
+
Дней бонуса
+
+ +
+ +
+{(stats?.referral_count || 0) >= 5 ? 30 : 0}
+
Milestone
+
+
+ + {/* Referral Link */} +
+

+ + Ваша реферальная ссылка +

+ +
+ + +
+ + +
+ + {/* Bonus Info */} +
+

💰 Как работают бонусы

+
    +
  • + + +7 дней за каждого приглашённого друга +
  • +
  • + + +30 дней бонус за каждые 5 рефералов +
  • +
  • + + Бонусы начисляются автоматически +
  • +
+
+ + {/* Referred Users */} + {referredUsers.length > 0 && ( +
+

+ + Ваши рефералы ({referredUsers.length}) +

+ +
+ {referredUsers.map((user, index) => ( +
+
+ {getStatusIcon(user.status)} +
+
{user.username}
+
+ {new Date(user.created_at).toLocaleDateString('ru-RU')} +
+
+
+
+ {getStatusText(user.status)} +
+
+ ))} +
+
+ )} + + {referredUsers.length === 0 && ( +
+ +

Пока нет рефералов

+

Поделитесь ссылкой с друзьями!

+
+ )} +
+ + {/* Toast Notification */} + {showToast && ( +
+
+ {toastMessage} +
+
+ )} +
+ ); +} diff --git a/components/ReferralModal.tsx b/components/ReferralModal.tsx index 8f9ce6b..97a8d2a 100644 --- a/components/ReferralModal.tsx +++ b/components/ReferralModal.tsx @@ -1,6 +1,7 @@ 'use client'; -import { X, Copy, Share2, Gift, Users, Award } from 'lucide-react'; +import { X, Copy, Share2, Gift, Users, Award, BarChart3 } from 'lucide-react'; +import Link from 'next/link'; interface ReferralModalProps { isOpen: boolean; @@ -105,10 +106,20 @@ export default function ReferralModal({ {/* Action buttons */}
+ + + +