feat: Добавить систему мультитенантности с регистрацией магазинов

Реализована полноценная система мультитенантности на базе django-tenants.
Каждый магазин получает изолированную схему БД и поддомен.

Основные компоненты:

Django-tenants интеграция:
- Модели Client (тенант) и Domain в приложении tenants/
- Разделение на SHARED_APPS и TENANT_APPS
- Public schema для общей админки
- Tenant schemas для изолированных данных магазинов

Система регистрации магазинов:
- Публичная форма регистрации на /register/
- Модель TenantRegistration для заявок со статусами (pending/approved/rejected)
- Валидация schema_name (латиница, 3-63 символа, уникальность)
- Проверка на зарезервированные имена (admin, api, www и т.д.)
- Админ-панель для модерации заявок с кнопками активации/отклонения

Система подписок:
- Модель Subscription с планами (триал 90 дней, месяц, квартал, год)
- Автоматическое создание триальной подписки при активации
- Методы is_expired() и days_left() для проверки статуса
- Цветовая индикация в админке (зеленый/оранжевый/красный)

Приложения:
- tenants/ - управление тенантами, регистрация, подписки
- shops/ - точки магазинов/самовывоза (tenant app)
- Обновлены миграции для всех приложений

Утилиты:
- switch_to_tenant.py - переключение между схемами тенантов
- Обновлены image_processor и image_service

Конфигурация:
- urls_public.py - роуты для public schema (админка + регистрация)
- urls.py - роуты для tenant schemas (магазины)
- requirements.txt - добавлены django-tenants, django-environ, phonenumber-field

Документация:
- DJANGO_TENANTS_SETUP.md - настройка мультитенантности
- TENANT_REGISTRATION_GUIDE.md - руководство по регистрации
- QUICK_START.md - быстрый старт
- START_HERE.md - общая документация

Использование:
1. Пользователь: http://localhost:8000/register/ → заполняет форму
2. Админ: http://localhost:8000/admin/ → активирует заявку
3. Результат: http://{schema_name}.localhost:8000/ - готовый магазин

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-27 19:13:10 +03:00
parent 4b44624f86
commit 097d4ea304
43 changed files with 3186 additions and 553 deletions

View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Регистрация магазина{% endblock %} - Inventory System</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px 0;
}
.card {
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
border: none;
border-radius: 15px;
}
.card-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 15px 15px 0 0 !important;
padding: 1.5rem;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
}
.btn-primary:hover {
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.form-control:focus {
border-color: #667eea;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
.alert {
border-radius: 10px;
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% block content %}{% endblock %}
</div>
</div>
</div>
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
{% block extra_js %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,133 @@
{% extends "tenants/base.html" %}
{% block title %}Регистрация нового магазина{% endblock %}
{% block content %}
<div class="card">
<div class="card-header text-center">
<h3 class="mb-0">Регистрация нового магазина</h3>
<p class="mb-0 mt-2">Заполните форму для создания вашего интернет-магазина</p>
</div>
<div class="card-body p-4">
<form method="post" novalidate>
{% csrf_token %}
<!-- Название магазина -->
<div class="mb-3">
<label for="{{ form.shop_name.id_for_label }}" class="form-label">
{{ form.shop_name.label }}
<span class="text-danger">*</span>
</label>
{{ form.shop_name }}
{% if form.shop_name.errors %}
<div class="text-danger small mt-1">
{{ form.shop_name.errors.0 }}
</div>
{% endif %}
{% if form.shop_name.help_text %}
<div class="form-text">{{ form.shop_name.help_text }}</div>
{% endif %}
</div>
<!-- Поддомен -->
<div class="mb-3">
<label for="{{ form.schema_name.id_for_label }}" class="form-label">
{{ form.schema_name.label }}
<span class="text-danger">*</span>
</label>
<div class="input-group">
{{ form.schema_name }}
<span class="input-group-text">.inventory.by</span>
</div>
{% if form.schema_name.errors %}
<div class="text-danger small mt-1">
{{ form.schema_name.errors.0 }}
</div>
{% endif %}
{% if form.schema_name.help_text %}
<div class="form-text">{{ form.schema_name.help_text }}</div>
{% endif %}
</div>
<!-- Имя владельца -->
<div class="mb-3">
<label for="{{ form.owner_name.id_for_label }}" class="form-label">
{{ form.owner_name.label }}
<span class="text-danger">*</span>
</label>
{{ form.owner_name }}
{% if form.owner_name.errors %}
<div class="text-danger small mt-1">
{{ form.owner_name.errors.0 }}
</div>
{% endif %}
</div>
<!-- Email -->
<div class="mb-3">
<label for="{{ form.owner_email.id_for_label }}" class="form-label">
{{ form.owner_email.label }}
<span class="text-danger">*</span>
</label>
{{ form.owner_email }}
{% if form.owner_email.errors %}
<div class="text-danger small mt-1">
{{ form.owner_email.errors.0 }}
</div>
{% endif %}
{% if form.owner_email.help_text %}
<div class="form-text">{{ form.owner_email.help_text }}</div>
{% endif %}
</div>
<!-- Телефон -->
<div class="mb-4">
<label for="{{ form.phone.id_for_label }}" class="form-label">
{{ form.phone.label }}
<span class="text-danger">*</span>
</label>
{{ form.phone }}
{% if form.phone.errors %}
<div class="text-danger small mt-1">
{{ form.phone.errors.0 }}
</div>
{% endif %}
</div>
<!-- Кнопка отправки -->
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg">
Отправить заявку
</button>
</div>
<div class="text-center mt-3">
<small class="text-muted">
После отправки заявки ваш магазин будет проверен администратором.<br>
Уведомление придет на указанный email в течение 24 часов.
</small>
</div>
</form>
</div>
</div>
<div class="text-center mt-3">
<a href="/admin/" class="text-white text-decoration-none">
<small>Войти в панель администратора</small>
</a>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Автоматическое преобразование поддомена в lowercase
document.addEventListener('DOMContentLoaded', function() {
const schemaNameInput = document.getElementById('{{ form.schema_name.id_for_label }}');
if (schemaNameInput) {
schemaNameInput.addEventListener('input', function() {
this.value = this.value.toLowerCase();
});
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,50 @@
{% extends "tenants/base.html" %}
{% block title %}Заявка отправлена{% endblock %}
{% block content %}
<div class="card">
<div class="card-header text-center">
<h3 class="mb-0">Спасибо за регистрацию!</h3>
</div>
<div class="card-body p-5 text-center">
<div class="mb-4">
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" fill="currentColor" class="bi bi-check-circle text-success" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"/>
</svg>
</div>
<h4 class="mb-3">Ваша заявка успешно отправлена!</h4>
<p class="lead mb-4">
Мы получили вашу заявку на создание магазина.<br>
Наш администратор проверит данные и активирует ваш магазин.
</p>
<div class="alert alert-info" role="alert">
<h5 class="alert-heading">Что дальше?</h5>
<hr>
<ul class="list-unstyled text-start mb-0">
<li class="mb-2">В течение 24 часов администратор проверит вашу заявку</li>
<li class="mb-2">✓ После активации вы получите письмо на указанный email</li>
<li class="mb-2">В письме будет ссылка на ваш магазин и инструкции</li>
<li>✓ Вам будет предоставлен триальный период на 90 дней</li>
</ul>
</div>
<div class="mt-4">
<a href="{% url 'tenants:register' %}" class="btn btn-outline-primary">
Подать еще одну заявку
</a>
</div>
<div class="mt-4">
<small class="text-muted">
Если у вас возникли вопросы, свяжитесь с нами:<br>
<a href="mailto:support@inventory.by">support@inventory.by</a>
</small>
</div>
</div>
</div>
{% endblock %}