feat(core): Wave 3.5 邀请制后端 — Invitation 模型 + 端点 + register 改造(只生成迁移不apply)
- Invitation 模型:team/code(TEAM-XXXX-XXXX唯一)/role/email/月额度/created_by/expires_at(7天)/ used_by/used_at/status(pending/used/revoked/expired);迁移 accounts/0003(未 apply 远程库,rule 3) - RegisterSerializer 加 invite_code:有效码→幂等 get_or_create 成员+标used+跳过额度发放; 无码→开新团队当 owner+试用额度(原逻辑);邀请 role 不许 OWNER 降级 ADMIN;不存在/已用/过期→400 - 端点 /api/auth/team/invitations/(GET列/POST生成)+ <id>/revoke/;权限复用 can_manage_team(超管/团管) 验证: django check 0;apps.accounts 测试 5/5 OK —— 含「凭码加入同团队/角色额度对/不重复发额度/ 未知码400/普通成员生成403」。认证定调:username 为主标识、不用邮箱注册。 ⚠️ Invitation 迁移需用户低峰 apply 到远程 MySQL,前端生成码 e2e 才能跑(逻辑已被测试证)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9643f9ba5e
commit
ba855cc219
@@ -12,8 +12,9 @@ from rest_framework.response import Response
|
||||
|
||||
from apps.common.api import get_current_team
|
||||
|
||||
from .models import LoginSession, TeamMember, User, UserPreference
|
||||
from .models import Invitation, LoginSession, TeamMember, User, UserPreference
|
||||
from .serializers import (
|
||||
InvitationSerializer,
|
||||
LoginSerializer,
|
||||
LoginSessionSerializer,
|
||||
RegisterSerializer,
|
||||
@@ -238,6 +239,45 @@ def team_members(request):
|
||||
return Response(TeamMemberSerializer(member).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
@api_view(["GET", "POST"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def team_invitations(request):
|
||||
"""团队邀请码:GET 列本团队邀请;POST 生成新码。权限=超管/团管(复用 can_manage_team)。"""
|
||||
team = get_current_team(request.user)
|
||||
if not can_manage_team(request.user, team):
|
||||
return Response({"detail": "permission denied"}, status=status.HTTP_403_FORBIDDEN)
|
||||
if request.method == "GET":
|
||||
invites = team.invitations.select_related("used_by").order_by("-created_at")
|
||||
return Response(InvitationSerializer(invites, many=True).data)
|
||||
role = normalize_member_role(request.data.get("role"))
|
||||
if role == TeamMember.Role.OWNER:
|
||||
role = TeamMember.Role.ADMIN # 邀请不允许 OWNER(Team.owner 唯一)
|
||||
invite = Invitation.objects.create(
|
||||
team=team,
|
||||
role=role,
|
||||
email=str(request.data.get("email") or "").strip(),
|
||||
monthly_credit_limit=request.data.get("monthly_credit_limit") or request.data.get("monthly") or 0,
|
||||
created_by=request.user,
|
||||
)
|
||||
return Response(InvitationSerializer(invite).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def revoke_invitation(request, invite_id):
|
||||
"""撤销一个待用邀请码。权限=超管/团管。"""
|
||||
team = get_current_team(request.user)
|
||||
if not can_manage_team(request.user, team):
|
||||
return Response({"detail": "permission denied"}, status=status.HTTP_403_FORBIDDEN)
|
||||
invite = Invitation.objects.filter(team=team, id=invite_id).first()
|
||||
if invite is None:
|
||||
return Response({"detail": "not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
if invite.status == Invitation.Status.PENDING:
|
||||
invite.status = Invitation.Status.REVOKED
|
||||
invite.save(update_fields=["status", "updated_at"])
|
||||
return Response(InvitationSerializer(invite).data)
|
||||
|
||||
|
||||
@api_view(["PATCH", "DELETE"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def team_member_detail(request, member_id):
|
||||
|
||||
Reference in New Issue
Block a user