PayrollManagementSystem/backend/app/schemas/user.py

114 lines
4.2 KiB
Python
Raw Normal View History

"""用户管理的请求/响应模型(角色相关模型见 `role.py`)。
与 `auth.py` 的分工:`auth.py` 管登录链路(验证码、登录、当前用户),
本模块管**用户管理**(增删改查、重置密码、启停、角色分配)。
字段名驼峰(规范约定)与蛇形属性之间用 alias 映射,与响应模型保持同一套做法。
"""
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field, field_validator
from app.core.constants import PASSWORD_MAX_BYTES, PASSWORD_MIN_BYTES
from app.core.response import PageQuery
def _validate_password_bytes(value: str) -> str:
"""按**字节数**校验密码长度。
必须在这里拦下:bcrypt 5.0 起对超过 72 字节的密码直接抛 ValueError,
而中文一个字符占 3 字节 —— 按字符数校验(如 max_length=20)根本拦不住。
与 `auth.py` 的登录校验共用同一规则,避免两处口径不一致。
"""
length = len(value.encode("utf-8"))
if length < PASSWORD_MIN_BYTES:
raise ValueError(f"密码不足 {PASSWORD_MIN_BYTES} 字节")
if length > PASSWORD_MAX_BYTES:
raise ValueError(f"密码超过 {PASSWORD_MAX_BYTES} 字节上限")
return value
class UserOut(BaseModel):
"""用户列表/详情响应。
不含 password —— 密码哈希绝不外泄,这是硬约束而非疏漏。
"""
model_config = ConfigDict(from_attributes=True)
id: int
username: str
real_name: str | None = None
phone: str | None = None
email: str | None = None
status: int = Field(description="状态:1启用 0禁用")
roles: list[str] = Field(default_factory=list, description="角色编码列表")
last_login_time: datetime | None = None
remark: str | None = None
create_time: datetime | None = None
class UserCreateIn(BaseModel):
"""新增用户。"""
model_config = ConfigDict(str_strip_whitespace=True)
username: str = Field(min_length=2, max_length=64, description="登录用户名,须唯一")
password: str = Field(description="初始密码")
real_name: str | None = Field(default=None, max_length=64)
phone: str | None = Field(default=None, max_length=20)
email: str | None = Field(default=None, max_length=128)
roles: list[str] = Field(default_factory=list, description="角色编码列表,至少一个")
remark: str | None = Field(default=None, max_length=255)
@field_validator("password")
@classmethod
def _check_password(cls, value: str) -> str:
return _validate_password_bytes(value)
class UserUpdateIn(BaseModel):
"""编辑用户。
**不含 username** —— 用户名是登录凭据与审计追溯的锚点,改名会让历史
操作日志与登录日志对不上。确需改名应走专门的流程(本期不提供)。
"""
model_config = ConfigDict(str_strip_whitespace=True)
real_name: str | None = Field(default=None, max_length=64)
phone: str | None = Field(default=None, max_length=20)
email: str | None = Field(default=None, max_length=128)
roles: list[str] | None = Field(default=None, description="传 null 表示不改角色")
remark: str | None = Field(default=None, max_length=255)
class PasswordResetIn(BaseModel):
"""重置密码(管理员操作,不需要原密码)。"""
new_password: str = Field(description="新密码")
@field_validator("new_password")
@classmethod
def _check_password(cls, value: str) -> str:
return _validate_password_bytes(value)
class UserStatusIn(BaseModel):
"""启用/禁用用户。"""
status: int = Field(ge=0, le=1, description="1启用 0禁用")
class UserQuery(PageQuery):
"""用户列表查询条件。
继承分页基类,额外支持按用户名/姓名模糊、按状态与角色筛选。
"""
username: str | None = Field(default=None, max_length=64, description="用户名,模糊匹配")
real_name: str | None = Field(default=None, max_length=64, description="姓名,模糊匹配")
status: int | None = Field(default=None, ge=0, le=1, description="状态筛选")
role_code: str | None = Field(default=None, max_length=32, description="角色编码筛选")