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

115
myproject/tenants/forms.py Normal file
View File

@@ -0,0 +1,115 @@
# -*- coding: utf-8 -*-
from django import forms
from django.core.exceptions import ValidationError
from .models import TenantRegistration, Client, RESERVED_SCHEMA_NAMES
class TenantRegistrationForm(forms.ModelForm):
"""
Форма регистрации нового тенанта
"""
class Meta:
model = TenantRegistration
fields = ['shop_name', 'schema_name', 'owner_name', 'owner_email', 'phone']
widgets = {
'shop_name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Название вашего магазина',
'required': True
}),
'schema_name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'myshop',
'required': True,
'pattern': '[a-z0-9][a-z0-9\-]{1,61}[a-z0-9]',
'title': 'Только латинские буквы в нижнем регистре, цифры и дефис (3-63 символа)'
}),
'owner_name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Иван Иванов',
'required': True
}),
'owner_email': forms.EmailInput(attrs={
'class': 'form-control',
'placeholder': 'your@email.com',
'required': True
}),
'phone': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '+375291234567',
'required': True
}),
}
labels = {
'shop_name': 'Название магазина',
'schema_name': 'Желаемый поддомен',
'owner_name': 'Ваше имя',
'owner_email': 'Email',
'phone': 'Телефон',
}
help_texts = {
'schema_name': 'Будет использоваться как поддомен: yourshop.inventory.by. '
'Только латинские буквы, цифры и дефис (3-63 символа)',
'owner_email': 'На этот email придет уведомление об активации',
}
def clean_schema_name(self):
"""
Валидация поддомена:
1. Приводим к нижнему регистру
2. Проверяем длину
3. Проверяем, что не зарезервировано
4. Проверяем уникальность
"""
schema_name = self.cleaned_data.get('schema_name', '').lower().strip()
# Проверка длины
if len(schema_name) < 3:
raise ValidationError("Поддомен должен содержать минимум 3 символа.")
if len(schema_name) > 63:
raise ValidationError("Поддомен не может быть длиннее 63 символов.")
# Проверка на зарезервированные имена
if schema_name in RESERVED_SCHEMA_NAMES:
raise ValidationError(f"Поддомен '{schema_name}' зарезервирован системой. Выберите другой.")
# Проверка уникальности в заявках
if TenantRegistration.objects.filter(schema_name=schema_name).exists():
raise ValidationError(f"Поддомен '{schema_name}' уже занят. Выберите другой.")
# Проверка уникальности в существующих тенантах
if Client.objects.filter(schema_name=schema_name).exists():
raise ValidationError(f"Поддомен '{schema_name}' уже занят. Выберите другой.")
return schema_name
def clean_owner_email(self):
"""
Валидация email: проверка на дубликаты для обычных пользователей
(супер-админ может иметь несколько тенантов)
"""
email = self.cleaned_data.get('owner_email', '').lower().strip()
# Проверяем, есть ли активные заявки с таким email
pending_registrations = TenantRegistration.objects.filter(
owner_email=email,
status=TenantRegistration.STATUS_PENDING
).count()
if pending_registrations > 0:
raise ValidationError(
f"У вас уже есть заявка на регистрацию с email {email}, ожидающая проверки. "
"Дождитесь активации или свяжитесь с поддержкой."
)
# Проверяем количество существующих тенантов с таким email
# Если больше 0 - это нормально для админов, но для обычных пользователей показываем предупреждение
existing_tenants_count = Client.objects.filter(owner_email=email).count()
if existing_tenants_count > 0:
# Добавляем предупреждение, но не блокируем (на случай если это супер-админ)
# В реальности здесь можно добавить более сложную логику
pass
return email