"""P0-3 · 公开注册:建租户 + 建用户 + owner membership + free 订阅 + JWT。 POST /api/v1/auth/register/(AllowAny,已进 TenantMiddleware 白名单): 入参 username / password / company_name /(可选)phone。 租户 code 由公司名 slug 化,冲突加数字后缀;非法 slug → 400。 密码最低 8 位;用户名已存在 → 400。 事务内:Tenant → User → TenantMembership(owner) → billing.subscribe(free)。 返回 JWT(access + refresh),免二次登录。 防滥用:同 IP 限速 10 次/小时(复用 core.ratelimit 的 cache 计数器)。 """ import re from adrf.decorators import api_view from django.db import transaction from django.utils.text import slugify from rest_framework import status from rest_framework.decorators import authentication_classes, permission_classes from rest_framework.exceptions import ValidationError from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework_simplejwt.tokens import RefreshToken REGISTER_IP_LIMIT = 10 REGISTER_IP_WINDOW = 3600 def _tenant_code_for(company_name: str) -> str: base = slugify(company_name, allow_unicode=False) or "" base = re.sub(r"[^a-z0-9-]", "", base.lower())[:50].strip("-") if not base: raise ValidationError({"company_name": "公司名称无法生成有效的租户编码,请换一个名称"}) from apps.core.models import Tenant code, i = base, 0 while Tenant.objects.filter(code=code).exists(): i += 1 code = f"{base}-{i}"[:64] return code @api_view(["POST"]) @authentication_classes([]) @permission_classes([AllowAny]) def register(request): from django.core.cache import cache from apps.core import ratelimit as rl ip = rl.client_ip(request) ip_key = f"dealerhub:register-ip:{ip}" if int(cache.get(ip_key) or 0) >= REGISTER_IP_LIMIT: return Response( {"code": "register_throttled", "detail": "同一 IP 注册过于频繁,请 1 小时后再试"}, status=status.HTTP_429_TOO_MANY_REQUESTS, ) payload = request.data or {} username = (payload.get("username") or "").strip() password = payload.get("password") or "" company_name = (payload.get("company_name") or "").strip() phone = (payload.get("phone") or "").strip() if not username or not password or not company_name: raise ValidationError({"detail": "username / password / company_name 均为必填"}) if len(password) < 8: raise ValidationError({"password": "密码长度至少 8 位"}) if len(username) > 150: raise ValidationError({"username": "用户名过长(最多 150 字符)"}) from django.contrib.auth import get_user_model User = get_user_model() if User.objects.filter(username=username).exists(): raise ValidationError({"username": "该用户名已被注册"}) code = _tenant_code_for(company_name) from apps.core.models import Tenant, TenantMembership with transaction.atomic(): tenant = Tenant.objects.create(code=code, name=company_name, phone=phone) user = User.objects.create_user(username=username, password=password) TenantMembership.objects.create( user=user, tenant=tenant, role="owner", is_active=True, ) try: from apps.billing.models import subscribe subscribe(tenant, plan_code="free") except Exception: pass try: cache.incr(ip_key) except ValueError: cache.set(ip_key, 1, REGISTER_IP_WINDOW) refresh = RefreshToken.for_user(user) return Response({ "access": str(refresh.access_token), "refresh": str(refresh), "tenant": tenant.code, "username": user.username, }, status=status.HTTP_201_CREATED)