Files
octopus/myproject/tenants/management/commands/create_tenant.py
Andrey Smakotin 097d4ea304 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>
2025-10-27 19:13:10 +03:00

159 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
Management команда для создания нового тенанта (магазина).
Использование:
python manage.py create_tenant
"""
from django.core.management.base import BaseCommand
from django.db import transaction
from tenants.models import Client, Domain
import re
class Command(BaseCommand):
help = 'Создать нового тенанта (магазин) с собственной схемой БД'
def handle(self, *args, **options):
self.stdout.write(self.style.SUCCESS('\n=== Создание нового магазина (тенанта) ===\n'))
# Получаем данные от пользователя
name = self.get_shop_name()
schema_name = self.get_schema_name()
domain_name = self.get_domain_name(schema_name)
owner_name = self.get_owner_name()
owner_email = self.get_owner_email()
phone = input('Телефон владельца (опционально): ').strip()
# Подтверждение
self.stdout.write('\n' + '='*60)
self.stdout.write(self.style.WARNING('Проверьте введенные данные:'))
self.stdout.write(f'Название магазина: {name}')
self.stdout.write(f'Схема БД: {schema_name}')
self.stdout.write(f'Домен: {domain_name}')
self.stdout.write(f'Владелец: {owner_name} ({owner_email})')
if phone:
self.stdout.write(f'Телефон: {phone}')
self.stdout.write('='*60 + '\n')
confirm = input('Создать магазин? (yes/no): ').strip().lower()
if confirm not in ['yes', 'y', 'да']:
self.stdout.write(self.style.ERROR('Отменено'))
return
# Создаем тенанта
try:
with transaction.atomic():
# Создаем тенанта
self.stdout.write('Создание тенанта...')
tenant = Client.objects.create(
schema_name=schema_name,
name=name,
owner_name=owner_name,
owner_email=owner_email,
phone=phone if phone else None,
)
self.stdout.write(self.style.SUCCESS(f'✓ Тенант создан: {tenant}'))
# Создаем домен
self.stdout.write(f'Создание домена {domain_name}...')
domain = Domain.objects.create(
domain=domain_name,
tenant=tenant,
is_primary=True
)
self.stdout.write(self.style.SUCCESS(f'✓ Домен создан: {domain}'))
self.stdout.write('\n' + '='*60)
self.stdout.write(self.style.SUCCESS('✓ Магазин успешно создан!'))
self.stdout.write('='*60 + '\n')
self.stdout.write(self.style.WARNING('Следующие шаги:'))
self.stdout.write(f'1. Добавьте в hosts файл: 127.0.0.1 {domain_name}')
self.stdout.write(f'2. Откройте в браузере: http://{domain_name}:8000/admin/')
self.stdout.write(f'3. Схема БД "{schema_name}" создана автоматически')
self.stdout.write(f'4. Все таблицы тенанта созданы в схеме "{schema_name}"')
self.stdout.write('')
except Exception as e:
self.stdout.write(self.style.ERROR(f'\n✗ Ошибка при создании тенанта: {e}'))
raise
def get_shop_name(self):
"""Получить название магазина"""
while True:
name = input('Название магазина: ').strip()
if name:
return name
self.stdout.write(self.style.ERROR('Название не может быть пустым'))
def get_schema_name(self):
"""Получить имя схемы БД"""
while True:
schema = input('Имя схемы БД (латиница, цифры, подчеркивания): ').strip().lower()
# Валидация
if not schema:
self.stdout.write(self.style.ERROR('Имя схемы не может быть пустым'))
continue
if not re.match(r'^[a-z0-9_]+$', schema):
self.stdout.write(self.style.ERROR('Только латинские буквы, цифры и подчеркивания'))
continue
if schema in ['public', 'postgres', 'information_schema', 'pg_catalog']:
self.stdout.write(self.style.ERROR('Это зарезервированное имя схемы'))
continue
# Проверка существования
if Client.objects.filter(schema_name=schema).exists():
self.stdout.write(self.style.ERROR(f'Схема "{schema}" уже существует'))
continue
return schema
def get_domain_name(self, default_subdomain):
"""Получить доменное имя"""
while True:
default_domain = f'{default_subdomain}.localhost'
domain = input(f'Доменное имя [{default_domain}]: ').strip().lower()
if not domain:
domain = default_domain
# Валидация
if not re.match(r'^[a-z0-9.-]+$', domain):
self.stdout.write(self.style.ERROR('Неверный формат домена'))
continue
# Проверка существования
if Domain.objects.filter(domain=domain).exists():
self.stdout.write(self.style.ERROR(f'Домен "{domain}" уже используется'))
continue
return domain
def get_owner_name(self):
"""Получить имя владельца"""
while True:
name = input('Имя владельца: ').strip()
if name:
return name
self.stdout.write(self.style.ERROR('Имя не может быть пустым'))
def get_owner_email(self):
"""Получить email владельца"""
while True:
email = input('Email владельца: ').strip().lower()
# Простая валидация email
if not email:
self.stdout.write(self.style.ERROR('Email не может быть пустым'))
continue
if '@' not in email or '.' not in email:
self.stdout.write(self.style.ERROR('Неверный формат email'))
continue
return email