first
This commit is contained in:
389
server.py
Normal file
389
server.py
Normal file
@@ -0,0 +1,389 @@
|
||||
# server.py
|
||||
import re
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from fastapi import FastAPI, Request, Form
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from typing import Optional
|
||||
|
||||
# ==================== КОНФИГУРАЦИЯ ====================
|
||||
HOST = "0.0.0.0"
|
||||
PORT = 8000
|
||||
DEBUG = True
|
||||
|
||||
BASE_DIR = Path(__file__).parent
|
||||
TEMPLATES_DIR = BASE_DIR / "templates"
|
||||
STATIC_DIR = BASE_DIR / "static"
|
||||
DATA_DIR = BASE_DIR / "data"
|
||||
|
||||
# Создаем необходимые папки
|
||||
TEMPLATES_DIR.mkdir(exist_ok=True)
|
||||
STATIC_DIR.mkdir(exist_ok=True)
|
||||
DATA_DIR.mkdir(exist_ok=True)
|
||||
|
||||
# Создаем подпапки для статики
|
||||
(STATIC_DIR / "css").mkdir(exist_ok=True)
|
||||
(STATIC_DIR / "js").mkdir(exist_ok=True)
|
||||
|
||||
# ==================== ИНИЦИАЛИЗАЦИЯ FASTAPI ====================
|
||||
app = FastAPI(title="БС Патриот - Регистрация на круглый стол", debug=DEBUG)
|
||||
|
||||
# Подключаем статические файлы
|
||||
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
||||
|
||||
|
||||
# ==================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ====================
|
||||
def save_registration_to_file(data: dict) -> dict:
|
||||
"""Сохраняет заявку в JSON-файл"""
|
||||
filepath = DATA_DIR / "registrations.json"
|
||||
|
||||
registrations = []
|
||||
if filepath.exists():
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
registrations = json.load(f)
|
||||
except (json.JSONDecodeError, IOError):
|
||||
registrations = []
|
||||
|
||||
new_record = {
|
||||
"id": len(registrations) + 1,
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"fullname": data.get("fullname"),
|
||||
"email": data.get("email"),
|
||||
"phone": data.get("phone"),
|
||||
"company": data.get("company"),
|
||||
"businessSize": data.get("businessSize", ""),
|
||||
"message": data.get("message", ""),
|
||||
"status": "pending"
|
||||
}
|
||||
|
||||
registrations.append(new_record)
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump(registrations, f, ensure_ascii=False, indent=2)
|
||||
|
||||
return new_record
|
||||
|
||||
|
||||
def get_html_content() -> str:
|
||||
"""Читает HTML файл или возвращает fallback"""
|
||||
html_path = TEMPLATES_DIR / "index.html"
|
||||
if html_path.exists():
|
||||
with open(html_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
else:
|
||||
return get_fallback_html()
|
||||
|
||||
|
||||
def get_fallback_html() -> str:
|
||||
"""Fallback HTML если файл шаблона не найден"""
|
||||
return """<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Круглый стол | БС «Патриот»</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{ font-family: 'Inter', Arial, sans-serif; background: #fefcf5; color: #1a1f2b; line-height: 1.6; }}
|
||||
.container {{ max-width: 1200px; margin: 0 auto; padding: 40px 20px; text-align: center; }}
|
||||
h1 {{ color: #c52a1c; margin-bottom: 20px; font-size: 2.5rem; }}
|
||||
.badge {{ background: rgba(197,42,28,0.12); color: #c52a1c; padding: 6px 18px; border-radius: 40px; display: inline-block; margin-bottom: 20px; }}
|
||||
.btn {{ display: inline-block; background: #c52a1c; color: white; padding: 12px 30px; text-decoration: none; border-radius: 50px; margin-top: 20px; border: none; cursor: pointer; font-family: inherit; }}
|
||||
.form-card {{ background: white; max-width: 500px; margin: 40px auto; padding: 30px; border-radius: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }}
|
||||
input, select, textarea {{ width: 100%; padding: 12px; margin: 10px 0; border: 1px solid #ddd; border-radius: 8px; font-family: inherit; }}
|
||||
button {{ background: #c52a1c; color: white; padding: 12px 30px; border: none; border-radius: 50px; cursor: pointer; font-size: 16px; font-family: inherit; width: 100%; }}
|
||||
.success {{ color: #1f8a4c; background: #e0f2e9; padding: 10px; border-radius: 8px; margin-top: 10px; }}
|
||||
.error {{ color: #c52a1c; background: #ffe8e5; padding: 10px; border-radius: 8px; margin-top: 10px; }}
|
||||
.stats {{ display: flex; justify-content: center; gap: 30px; margin: 30px 0; flex-wrap: wrap; }}
|
||||
.stat {{ text-align: center; }}
|
||||
.stat h4 {{ font-size: 2rem; color: #c52a1c; }}
|
||||
footer {{ background: #0a1a2f; color: #b9c7d9; padding: 30px; margin-top: 40px; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="badge"><i class="fas fa-shield-alt"></i> Закрытый клуб предпринимателей</div>
|
||||
<h1>Круглый стол с бизнес-сообществом <span style="color:#c52a1c;">«Патриот»</span></h1>
|
||||
<p><i class="far fa-calendar-check"></i> <strong>26 апреля 2026, начало в 11:00</strong></p>
|
||||
<p><i class="fas fa-lock"></i> Вход по рекомендации</p>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat"><h4>557+</h4><p>участников</p></div>
|
||||
<div class="stat"><h4>125+</h4><p>выездов в зону СВО</p></div>
|
||||
<div class="stat"><h4>30+</h4><p>комитетов</p></div>
|
||||
</div>
|
||||
|
||||
<div class="form-card">
|
||||
<h2>Регистрация на мероприятие</h2>
|
||||
<form id="registrationForm">
|
||||
<input type="text" name="fullname" id="fullname" placeholder="ФИО *" required>
|
||||
<input type="email" name="email" id="email" placeholder="E-mail *" required>
|
||||
<input type="tel" name="phone" id="phone" placeholder="Телефон *" required>
|
||||
<input type="text" name="company" id="company" placeholder="Компания *" required>
|
||||
<select name="businessSize" id="businessSize">
|
||||
<option value="">Годовой оборот (опционально)</option>
|
||||
<option value="до 50 млн руб.">до 50 млн руб.</option>
|
||||
<option value="50–500 млн руб.">50–500 млн руб.</option>
|
||||
<option value="500 млн – 1 млрд руб.">500 млн – 1 млрд руб.</option>
|
||||
<option value="более 1 млрд руб.">более 1 млрд руб.</option>
|
||||
</select>
|
||||
<textarea name="message" id="message" rows="3" placeholder="Коротко о себе"></textarea>
|
||||
<label>
|
||||
<input type="checkbox" name="agree" id="agreeCheck" value="true" required>
|
||||
Я соглашаюсь с условиями *
|
||||
</label>
|
||||
<button type="submit">Отправить заявку</button>
|
||||
<div id="formFeedback"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<p>Бизнес-сообщество «Предприниматели Патриоты»</p>
|
||||
<p>© 2026 Круглый стол с БС «Патриот»</p>
|
||||
</footer>
|
||||
<script>
|
||||
const form = document.getElementById('registrationForm');
|
||||
form.addEventListener('submit', async (e) => {{
|
||||
e.preventDefault();
|
||||
const feedback = document.getElementById('formFeedback');
|
||||
const formData = new FormData(form);
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
const originalText = submitBtn.innerHTML;
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-pulse"></i> Отправка...';
|
||||
|
||||
try {{
|
||||
const response = await fetch('/api/register', {{
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}});
|
||||
const data = await response.json();
|
||||
if (data.success) {{
|
||||
feedback.innerHTML = '<div class="success">✅ ' + data.message + '</div>';
|
||||
form.reset();
|
||||
}} else {{
|
||||
feedback.innerHTML = '<div class="error">❌ ' + data.message + '</div>';
|
||||
}}
|
||||
}} catch (err) {{
|
||||
console.error('Error:', err);
|
||||
feedback.innerHTML = '<div class="error">❌ Ошибка соединения</div>';
|
||||
}} finally {{
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = originalText;
|
||||
setTimeout(() => {{ feedback.innerHTML = ''; }}, 5000);
|
||||
}}
|
||||
}});
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
# ==================== МАРШРУТЫ ====================
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def get_landing_page():
|
||||
"""Главная страница - посадочный лендинг мероприятия"""
|
||||
return HTMLResponse(content=get_html_content(), status_code=200)
|
||||
|
||||
|
||||
@app.post("/api/register")
|
||||
async def register(
|
||||
fullname: str = Form(...),
|
||||
email: str = Form(...),
|
||||
phone: str = Form(...),
|
||||
company: str = Form(...),
|
||||
businessSize: Optional[str] = Form(None),
|
||||
message: Optional[str] = Form(None),
|
||||
agree: Optional[str] = Form(None)
|
||||
):
|
||||
"""API-эндпоинт для регистрации участников"""
|
||||
|
||||
print(f"Received data: fullname={fullname}, email={email}, phone={phone}, company={company}, agree={agree}")
|
||||
|
||||
# Валидация
|
||||
if not agree or agree != "true":
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"success": False, "message": "Необходимо согласие с условиями"}
|
||||
)
|
||||
|
||||
if not fullname or len(fullname.strip()) < 2:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"success": False, "message": "Введите корректное ФИО"}
|
||||
)
|
||||
|
||||
email_pattern = r'^[^\s@]+@([^\s@.,]+\.)+[^\s@.,]{2,}$'
|
||||
if not re.match(email_pattern, email):
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"success": False, "message": "Введите корректный email"}
|
||||
)
|
||||
|
||||
digits_only = re.sub(r'\D', '', phone)
|
||||
if len(digits_only) < 10:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"success": False, "message": "Введите корректный номер телефона (минимум 10 цифр)"}
|
||||
)
|
||||
|
||||
if not company or len(company.strip()) < 1:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"success": False, "message": "Укажите название компании"}
|
||||
)
|
||||
|
||||
data = {
|
||||
"fullname": fullname.strip(),
|
||||
"email": email.strip().lower(),
|
||||
"phone": phone.strip(),
|
||||
"company": company.strip(),
|
||||
"businessSize": businessSize if businessSize else "",
|
||||
"message": message.strip() if message else "",
|
||||
}
|
||||
|
||||
try:
|
||||
saved = save_registration_to_file(data)
|
||||
print(f"Saved registration: {saved}")
|
||||
return JSONResponse(content={
|
||||
"success": True,
|
||||
"message": "Заявка успешно отправлена! Менеджер сообщества свяжется с вами в ближайшее время."
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error saving registration: {e}")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"success": False, "message": f"Ошибка сервера: {str(e)}"}
|
||||
)
|
||||
|
||||
|
||||
@app.get("/admin/registrations", response_class=HTMLResponse)
|
||||
async def admin_registrations():
|
||||
"""Админка для просмотра заявок"""
|
||||
filepath = DATA_DIR / "registrations.json"
|
||||
registrations = []
|
||||
if filepath.exists():
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
registrations = json.load(f)
|
||||
except:
|
||||
registrations = []
|
||||
|
||||
registrations.sort(key=lambda x: x.get("created_at", ""), reverse=True)
|
||||
|
||||
# Используем обычное форматирование строки без .format()
|
||||
html = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Админка | Заявки на круглый стол</title>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{ font-family: 'Inter', Arial, sans-serif; margin: 40px; background: #f5f5f5; }}
|
||||
h1 {{ color: #c52a1c; margin-bottom: 20px; }}
|
||||
.container {{ max-width: 1400px; margin: 0 auto; background: white; border-radius: 24px; padding: 24px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }}
|
||||
table {{ border-collapse: collapse; width: 100%; margin-top: 20px; }}
|
||||
th, td {{ border: 1px solid #ddd; padding: 12px; text-align: left; vertical-align: top; }}
|
||||
th {{ background: #c52a1c; color: white; position: sticky; top: 0; }}
|
||||
tr:nth-child(even) {{ background: #f9f9f9; }}
|
||||
.status-pending {{ color: #e67e22; font-weight: bold; }}
|
||||
.count {{ margin: 20px 0; font-size: 1.1rem; }}
|
||||
.back-link {{ display: inline-block; margin-top: 20px; color: #c52a1c; text-decoration: none; font-weight: 600; }}
|
||||
.back-link:hover {{ text-decoration: underline; }}
|
||||
@media (max-width: 768px) {{
|
||||
body {{ margin: 20px; }}
|
||||
.container {{ padding: 16px; overflow-x: auto; }}
|
||||
th, td {{ padding: 8px; font-size: 12px; }}
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>📋 Заявки на круглый стол «Патриот»</h1>
|
||||
<div class="count">📊 Всего заявок: <strong>{len(registrations)}</strong></div>
|
||||
<div style="overflow-x: auto;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Дата</th>
|
||||
<th>ФИО</th>
|
||||
<th>Email</th>
|
||||
<th>Телефон</th>
|
||||
<th>Компания</th>
|
||||
<th>Оборот</th>
|
||||
<th>Сообщение</th>
|
||||
<th>Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
"""
|
||||
|
||||
for reg in registrations:
|
||||
created = reg.get("created_at", "")[:16].replace("T", " ")
|
||||
status_class = "status-pending" if reg.get('status') == 'pending' else ""
|
||||
message_text = reg.get('message', '—')[:100]
|
||||
if len(reg.get('message', '')) > 100:
|
||||
message_text += "..."
|
||||
|
||||
html += f"""
|
||||
<tr>
|
||||
<td>{reg.get('id', '—')}</td>
|
||||
<td>{created}</td>
|
||||
<td>{reg.get('fullname', '—')}</td>
|
||||
<td>{reg.get('email', '—')}</td>
|
||||
<td>{reg.get('phone', '—')}</td>
|
||||
<td>{reg.get('company', '—')}</td>
|
||||
<td>{reg.get('businessSize', '—')}</td>
|
||||
<td>{message_text}</td>
|
||||
<td class="{status_class}">{reg.get('status', 'pending')}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
html += """
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<a href="/" class="back-link">← Вернуться на главную</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
return HTMLResponse(content=html, status_code=200)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""Health check для хостинга"""
|
||||
return {"status": "ok", "timestamp": datetime.now().isoformat()}
|
||||
|
||||
|
||||
# ==================== ЗАПУСК ====================
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
print("=" * 60)
|
||||
print("🚀 Сервер для посадочной страницы БС «Патриот»")
|
||||
print(f"📁 Шаблоны: {TEMPLATES_DIR}")
|
||||
print(f"📁 Статика: {STATIC_DIR}")
|
||||
print(f"📁 Данные: {DATA_DIR}")
|
||||
print("=" * 60)
|
||||
print(f"✨ Главная страница: http://localhost:{PORT}")
|
||||
print(f"🔧 Админка: http://localhost:{PORT}/admin/registrations")
|
||||
print("=" * 60)
|
||||
print("💡 Для остановки сервера нажмите Ctrl+C")
|
||||
print("=" * 60)
|
||||
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=HOST,
|
||||
port=PORT,
|
||||
log_level="info"
|
||||
)
|
||||
Reference in New Issue
Block a user