"""角色管理的请求/响应模型。 `RoleOut` 原位于 `schemas/user.py`,因角色管理成为独立模块而迁入本文件 —— 用户表单的角色下拉与角色管理列表共用同一响应模型。 """ import re from pydantic import BaseModel, ConfigDict, Field, field_validator from app.core.response import PageQuery # 角色编码规则:小写字母开头,后接小写字母/数字/下划线,长度 2–32(与列宽一致)。 # 不允许大写与中文 —— 编码会出现在授权判断(`ROLE_ADMIN` 常量按编码比对)与 # 操作日志里,保持单一风格避免大小写/全角混用造成对照困难 ROLE_CODE_PATTERN = re.compile(r"^[a-z][a-z0-9_]{1,31}$") class RoleOut(BaseModel): """角色基础响应(用户表单下拉与角色列表共用)。""" model_config = ConfigDict(from_attributes=True) id: int role_code: str role_name: str description: str | None = None sort_order: int = 0 status: int = 1 class RoleManageOut(RoleOut): """角色管理列表项:额外携带绑定用户数,供前端提示删除保护的原因。""" user_count: int = 0 permission_count: int = 0 class RoleCreateIn(BaseModel): """新增角色。""" model_config = ConfigDict(str_strip_whitespace=True) role_code: str = Field(min_length=2, max_length=32, description="角色编码,创建后不可修改") role_name: str = Field(min_length=1, max_length=64, description="角色名称") description: str | None = Field(default=None, max_length=255) sort_order: int = Field(default=0, ge=0, description="显示排序,越小越靠前") permission_codes: list[str] | None = Field(default=None, max_length=300) reason: str | None = Field(default=None, min_length=1, max_length=500) @field_validator("role_code") @classmethod def _check_code(cls, value: str) -> str: if not ROLE_CODE_PATTERN.match(value): raise ValueError("角色编码须以小写字母开头,仅可含小写字母、数字与下划线") return value class RoleUpdateIn(BaseModel): """编辑角色。 **不含 role_code** —— 编码是授权判定的锚点,修改会让既有授权语义静默改变。 前端表单全量提交,故为全量替换语义。 """ model_config = ConfigDict(str_strip_whitespace=True) role_name: str = Field(min_length=1, max_length=64, description="角色名称") description: str | None = Field(default=None, max_length=255) sort_order: int = Field(default=0, ge=0, description="显示排序,越小越靠前") class RoleStatusIn(BaseModel): """启用/禁用角色。""" status: int = Field(ge=0, le=1, description="1启用 0禁用") class RoleQuery(PageQuery): """角色列表查询条件。""" keyword: str | None = Field(default=None, max_length=64, description="编码或名称,模糊匹配") status: int | None = Field(default=None, ge=0, le=1, description="状态筛选")