157 lines
3.8 KiB
Python
157 lines
3.8 KiB
Python
"""
|
||
用户相关 Schema
|
||
|
||
定义用户数据的验证和序列化规则。
|
||
"""
|
||
|
||
import re
|
||
from datetime import datetime
|
||
from typing import Annotated
|
||
|
||
from pydantic import EmailStr, Field, field_validator
|
||
|
||
from app.core.config import settings
|
||
from app.schemas.base import BaseSchema
|
||
|
||
|
||
# 用户名正则:字母开头,只允许字母、数字、下划线
|
||
USERNAME_PATTERN = re.compile(r"^[a-zA-Z][a-zA-Z0-9_]*$")
|
||
|
||
|
||
class UserBase(BaseSchema):
|
||
"""用户基础字段"""
|
||
|
||
username: Annotated[
|
||
str,
|
||
Field(
|
||
min_length=settings.username_min_length,
|
||
max_length=settings.username_max_length,
|
||
description="用户名(字母开头,只允许字母、数字、下划线)",
|
||
examples=["john_doe"],
|
||
),
|
||
]
|
||
email: Annotated[
|
||
EmailStr | None,
|
||
Field(
|
||
default=None,
|
||
description="邮箱地址",
|
||
examples=["user@example.com"],
|
||
),
|
||
]
|
||
nickname: Annotated[
|
||
str | None,
|
||
Field(
|
||
default=None,
|
||
max_length=64,
|
||
description="昵称",
|
||
examples=["John"],
|
||
),
|
||
]
|
||
|
||
@field_validator("username")
|
||
@classmethod
|
||
def validate_username(cls, v: str) -> str:
|
||
"""验证用户名格式"""
|
||
if not USERNAME_PATTERN.match(v):
|
||
raise ValueError("用户名必须以字母开头,只能包含字母、数字和下划线")
|
||
return v.lower() # 统一转小写
|
||
|
||
|
||
class UserCreate(UserBase):
|
||
"""用户注册请求"""
|
||
|
||
password: Annotated[
|
||
str,
|
||
Field(
|
||
min_length=settings.password_min_length,
|
||
max_length=settings.password_max_length,
|
||
description="密码",
|
||
examples=["SecurePass123"],
|
||
),
|
||
]
|
||
|
||
@field_validator("password")
|
||
@classmethod
|
||
def validate_password(cls, v: str) -> str:
|
||
"""验证密码强度"""
|
||
errors: list[str] = []
|
||
|
||
if settings.password_require_uppercase and not re.search(r"[A-Z]", v):
|
||
errors.append("至少包含一个大写字母")
|
||
|
||
if settings.password_require_lowercase and not re.search(r"[a-z]", v):
|
||
errors.append("至少包含一个小写字母")
|
||
|
||
if settings.password_require_digit and not re.search(r"\d", v):
|
||
errors.append("至少包含一个数字")
|
||
|
||
if settings.password_require_special and not re.search(r"[!@#$%^&*(),.?\":{}|<>]", v):
|
||
errors.append("至少包含一个特殊字符")
|
||
|
||
if errors:
|
||
raise ValueError("密码强度不足:" + ";".join(errors))
|
||
|
||
return v
|
||
|
||
|
||
class UserUpdate(BaseSchema):
|
||
"""用户信息更新请求"""
|
||
|
||
nickname: Annotated[
|
||
str | None,
|
||
Field(
|
||
default=None,
|
||
max_length=64,
|
||
description="昵称",
|
||
),
|
||
]
|
||
email: Annotated[
|
||
EmailStr | None,
|
||
Field(
|
||
default=None,
|
||
description="邮箱地址",
|
||
),
|
||
]
|
||
avatar_url: Annotated[
|
||
str | None,
|
||
Field(
|
||
default=None,
|
||
max_length=512,
|
||
description="头像 URL",
|
||
),
|
||
]
|
||
bio: Annotated[
|
||
str | None,
|
||
Field(
|
||
default=None,
|
||
max_length=500,
|
||
description="个人简介",
|
||
),
|
||
]
|
||
|
||
|
||
class UserResponse(BaseSchema):
|
||
"""用户信息响应"""
|
||
|
||
id: str
|
||
username: str
|
||
email: str | None
|
||
nickname: str | None
|
||
avatar_url: str | None
|
||
bio: str | None
|
||
is_active: bool
|
||
created_at: datetime
|
||
last_login_at: datetime | None
|
||
|
||
|
||
class UserProfileResponse(BaseSchema):
|
||
"""用户公开资料响应(不包含敏感信息)"""
|
||
|
||
id: str
|
||
username: str
|
||
nickname: str | None
|
||
avatar_url: str | None
|
||
bio: str | None
|
||
created_at: datetime
|
||
|