From 6519fee7296d0bed2fb3cacf62c3c3c4c14d3a2f Mon Sep 17 00:00:00 2001 From: chengkai3 Date: Thu, 18 Jun 2026 00:34:03 +0800 Subject: [PATCH] =?UTF-8?q?[feat]:[FL-172][=E7=94=A8=E6=88=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E9=A1=B5=E9=9D=A2=E6=96=B0=E5=A2=9E=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=97=B6=E9=82=AE=E7=AE=B1=E6=94=BE=E6=9C=80=E5=90=8E=EF=BC=8C?= =?UTF-8?q?=E4=B8=94=E4=B8=8D=E8=A6=81=E5=BF=85=E5=A1=AB]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 Co-authored-by: multica-agent --- api/app/models/user.py | 2 +- api/app/schemas/user.py | 2 +- api/app/services/user_service.py | 11 +++++---- migrations/make_user_email_nullable.sql | 9 +++++++ web/src/app/admin/users/page.tsx | 32 +++++++++++++------------ 5 files changed, 35 insertions(+), 21 deletions(-) create mode 100644 migrations/make_user_email_nullable.sql diff --git a/api/app/models/user.py b/api/app/models/user.py index f2ac271..bb30f09 100644 --- a/api/app/models/user.py +++ b/api/app/models/user.py @@ -28,7 +28,7 @@ class User(Base): primary_key=True, default=lambda: uuid4().hex, ) - email: Mapped[str] = mapped_column(String(255), unique=True, index=True) + email: Mapped[str | None] = mapped_column(String(255), unique=True, index=True, nullable=True) username: Mapped[str] = mapped_column( settings.user_username_column, String(64), diff --git a/api/app/schemas/user.py b/api/app/schemas/user.py index a510442..cbf5fb3 100644 --- a/api/app/schemas/user.py +++ b/api/app/schemas/user.py @@ -36,7 +36,7 @@ class UserPasswordResetRequest(BaseModel): class UserCreateRequest(BaseModel): user_id: str = Field(min_length=3, max_length=64) - email: str + email: str | None = None username: str = Field(min_length=3, max_length=64) password: str = Field(min_length=8, max_length=128) diff --git a/api/app/services/user_service.py b/api/app/services/user_service.py index d04044c..2d03dd8 100644 --- a/api/app/services/user_service.py +++ b/api/app/services/user_service.py @@ -95,17 +95,20 @@ def create_user( ) -> UserPublic | None: user_id = payload.user_id.strip() + # Build conditions for duplicate check + conditions = [User.id == user_id, User.username == payload.username] + if payload.email: + conditions.append(User.email == payload.email.lower()) + duplicate = db.scalar( - select(User.id).where( - (User.id == user_id) | (User.email == payload.email.lower()) | (User.username == payload.username) - ) + select(User.id).where(or_(*conditions)) ) if duplicate: return None user = User( id=user_id, - email=payload.email.lower(), + email=payload.email.lower() if payload.email else None, username=payload.username, password_hash=hash_password(payload.password), status="ENABLED", diff --git a/migrations/make_user_email_nullable.sql b/migrations/make_user_email_nullable.sql new file mode 100644 index 0000000..7c77d72 --- /dev/null +++ b/migrations/make_user_email_nullable.sql @@ -0,0 +1,9 @@ +-- Migration: Make user email column nullable +-- Date: 2026-06-18 +-- Description: Modify users table to make email column nullable to support optional email during user creation + +ALTER TABLE users ALTER COLUMN email DROP NOT NULL; + +-- Notes: +-- This migration allows users to be created without an email address. +-- The email field remains unique when provided. diff --git a/web/src/app/admin/users/page.tsx b/web/src/app/admin/users/page.tsx index 241581b..e9519ff 100644 --- a/web/src/app/admin/users/page.tsx +++ b/web/src/app/admin/users/page.tsx @@ -39,7 +39,7 @@ type UserRolePayload = { type CreateUserValues = { user_id: string; - email: string; + email?: string; username: string; password: string; }; @@ -323,20 +323,23 @@ export default function AdminUsersPage() { const payload: CreateUserValues = { user_id: values.user_id.trim(), - email: values.email.trim(), username: values.username.trim(), password: values.password, }; + if (values.email && values.email.trim()) { + payload.email = values.email.trim(); + } + const candidateUserId = payload.user_id.toLowerCase(); - const candidateEmail = payload.email.toLowerCase(); + const candidateEmail = payload.email?.toLowerCase(); const candidateUsername = payload.username.toLowerCase(); if (existingUserIds.has(candidateUserId)) { setError("用户 ID 已存在,请更换后重试"); return; } - if (existingEmails.has(candidateEmail)) { + if (candidateEmail && existingEmails.has(candidateEmail)) { setError("邮箱已存在,请更换后重试"); return; } @@ -796,17 +799,6 @@ export default function AdminUsersPage() { - - - - + + + +