mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-26 22:31:21 +00:00
feat: enforce required department and role codes with validation
- Updated DeptModel and RoleModel to make the `code` field mandatory and unique. - Enhanced DeptCreateSchema and RoleCreateSchema to require a valid `code` format, ensuring it starts with a letter and is 2-16 characters long. - Implemented validation logic in the backend to check for existing codes during updates, preventing duplicates. - Adjusted frontend forms to reflect the required `code` field and added validation messages for user guidance.
This commit is contained in:
@@ -23,8 +23,8 @@ class DeptModel(ModelMixin):
|
|||||||
|
|
||||||
name: Mapped[str] = mapped_column(String(64), nullable=False, comment="部门名称")
|
name: Mapped[str] = mapped_column(String(64), nullable=False, comment="部门名称")
|
||||||
order: Mapped[int] = mapped_column(Integer, nullable=False, default=999, comment="显示排序")
|
order: Mapped[int] = mapped_column(Integer, nullable=False, default=999, comment="显示排序")
|
||||||
code: Mapped[str | None] = mapped_column(
|
code: Mapped[str] = mapped_column(
|
||||||
String(16), nullable=True, index=True, comment="部门编码"
|
String(16), nullable=False, unique=True, comment="部门编码"
|
||||||
)
|
)
|
||||||
leader: Mapped[str | None] = mapped_column(String(32), default=None, comment="部门负责人")
|
leader: Mapped[str | None] = mapped_column(String(32), default=None, comment="部门负责人")
|
||||||
phone: Mapped[str | None] = mapped_column(String(11), default=None, comment="手机")
|
phone: Mapped[str | None] = mapped_column(String(11), default=None, comment="手机")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|||||||
|
|
||||||
from app.common.enums import QueueEnum
|
from app.common.enums import QueueEnum
|
||||||
from app.core.base_schema import BaseSchema
|
from app.core.base_schema import BaseSchema
|
||||||
from app.core.validator import DateTimeStr
|
from app.core.validator import DateTimeStr, validate_required_code
|
||||||
|
|
||||||
|
|
||||||
class DeptCreateSchema(BaseModel):
|
class DeptCreateSchema(BaseModel):
|
||||||
@@ -11,7 +11,7 @@ class DeptCreateSchema(BaseModel):
|
|||||||
|
|
||||||
name: str = Field(..., max_length=64, description="部门名称")
|
name: str = Field(..., max_length=64, description="部门名称")
|
||||||
order: int = Field(default=1, ge=0, description="显示顺序")
|
order: int = Field(default=1, ge=0, description="显示顺序")
|
||||||
code: str | None = Field(default=None, max_length=16, description="部门编码")
|
code: str = Field(..., max_length=16, description="部门编码")
|
||||||
leader: str | None = Field(default=None, max_length=32, description="部门负责人")
|
leader: str | None = Field(default=None, max_length=32, description="部门负责人")
|
||||||
phone: str | None = Field(default=None, max_length=11, description="手机")
|
phone: str | None = Field(default=None, max_length=11, description="手机")
|
||||||
email: str | None = Field(default=None, max_length=64, description="邮箱")
|
email: str | None = Field(default=None, max_length=64, description="邮箱")
|
||||||
@@ -41,29 +41,20 @@ class DeptCreateSchema(BaseModel):
|
|||||||
|
|
||||||
@field_validator("code")
|
@field_validator("code")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_code(cls, value: str | None):
|
def validate_code(cls, value: str):
|
||||||
"""
|
"""
|
||||||
校验部门编码:字母开头,仅包含字母/数字/下划线;空字符串视为 None。
|
校验部门编码(与角色编码规则一致,见 `validate_required_code`)。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- value (str | None): 部门编码。
|
- value (str): 部门编码。
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- str | None: 规范化后的部门编码或 None。
|
- str: 规范化后的部门编码。
|
||||||
|
|
||||||
异常:
|
异常:
|
||||||
- ValueError: 编码不满足格式要求时抛出。
|
- ValueError: 编码不满足格式要求时抛出。
|
||||||
"""
|
"""
|
||||||
if value is None:
|
return validate_required_code(value)
|
||||||
return value
|
|
||||||
v = value.strip()
|
|
||||||
if v == "":
|
|
||||||
return None
|
|
||||||
import re
|
|
||||||
|
|
||||||
if not re.match(r"^[A-Za-z][A-Za-z0-9_]*$", v):
|
|
||||||
raise ValueError("部门编码必须以字母开头,且仅包含字母/数字/下划线")
|
|
||||||
return v
|
|
||||||
|
|
||||||
|
|
||||||
class DeptUpdateSchema(DeptCreateSchema):
|
class DeptUpdateSchema(DeptCreateSchema):
|
||||||
|
|||||||
@@ -116,6 +116,9 @@ class DeptService:
|
|||||||
exist_dept = await DeptCRUD(auth).get(name=data.name)
|
exist_dept = await DeptCRUD(auth).get(name=data.name)
|
||||||
if exist_dept and exist_dept.id != id:
|
if exist_dept and exist_dept.id != id:
|
||||||
raise CustomException(msg="更新失败,部门名称重复")
|
raise CustomException(msg="更新失败,部门名称重复")
|
||||||
|
exist_code = await DeptCRUD(auth).get(code=data.code)
|
||||||
|
if exist_code and exist_code.id != id:
|
||||||
|
raise CustomException(msg="更新失败,部门编码已存在")
|
||||||
dept = await DeptCRUD(auth).update(id=id, data=data)
|
dept = await DeptCRUD(auth).update(id=id, data=data)
|
||||||
return DeptOutSchema.model_validate(dept).model_dump()
|
return DeptOutSchema.model_validate(dept).model_dump()
|
||||||
|
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ class RoleModel(ModelMixin):
|
|||||||
__permission_strategy__: PermissionFilterStrategy = PermissionFilterStrategy.USER_ROLE
|
__permission_strategy__: PermissionFilterStrategy = PermissionFilterStrategy.USER_ROLE
|
||||||
|
|
||||||
name: Mapped[str] = mapped_column(String(64), nullable=False, comment="角色名称")
|
name: Mapped[str] = mapped_column(String(64), nullable=False, comment="角色名称")
|
||||||
code: Mapped[str | None] = mapped_column(
|
code: Mapped[str] = mapped_column(
|
||||||
String(16), nullable=True, index=True, comment="角色编码"
|
String(16), nullable=False, unique=True, comment="角色编码"
|
||||||
)
|
)
|
||||||
order: Mapped[int] = mapped_column(Integer, nullable=False, default=999, comment="显示排序")
|
order: Mapped[int] = mapped_column(Integer, nullable=False, default=999, comment="显示排序")
|
||||||
data_scope: Mapped[int] = mapped_column(
|
data_scope: Mapped[int] = mapped_column(
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ from app.common.enums import QueueEnum
|
|||||||
from app.core.base_schema import BaseSchema
|
from app.core.base_schema import BaseSchema
|
||||||
from app.core.validator import (
|
from app.core.validator import (
|
||||||
DateTimeStr,
|
DateTimeStr,
|
||||||
code_validator,
|
|
||||||
role_permission_request_validator,
|
role_permission_request_validator,
|
||||||
|
validate_required_code,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ class RoleCreateSchema(BaseModel):
|
|||||||
"""角色创建模型"""
|
"""角色创建模型"""
|
||||||
|
|
||||||
name: str = Field(..., max_length=64, description="角色名称")
|
name: str = Field(..., max_length=64, description="角色名称")
|
||||||
code: str | None = Field(default=None, max_length=16, description="角色编码")
|
code: str = Field(..., max_length=16, description="角色编码")
|
||||||
order: int | None = Field(default=1, ge=1, description="显示排序")
|
order: int | None = Field(default=1, ge=1, description="显示排序")
|
||||||
data_scope: int | None = Field(
|
data_scope: int | None = Field(
|
||||||
default=1,
|
default=1,
|
||||||
@@ -33,20 +33,20 @@ class RoleCreateSchema(BaseModel):
|
|||||||
|
|
||||||
@field_validator("code")
|
@field_validator("code")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_code(cls, value: str | None):
|
def validate_code(cls, value: str):
|
||||||
"""
|
"""
|
||||||
校验角色编码(委托到 `code_validator`)。
|
校验角色编码(与部门编码规则一致,见 `validate_required_code`)。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- value (str | None): 角色编码。
|
- value (str): 角色编码。
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- str | None: 校验后的角色编码。
|
- str: 校验后的角色编码。
|
||||||
|
|
||||||
异常:
|
异常:
|
||||||
- CustomException: 不满足编码规则时抛出。
|
- ValueError: 不满足编码规则时抛出。
|
||||||
"""
|
"""
|
||||||
return code_validator(value)
|
return validate_required_code(value)
|
||||||
|
|
||||||
|
|
||||||
class RolePermissionSettingSchema(BaseModel):
|
class RolePermissionSettingSchema(BaseModel):
|
||||||
|
|||||||
@@ -125,6 +125,9 @@ class RoleService:
|
|||||||
exist_role = await RoleCRUD(auth).get(name=data.name)
|
exist_role = await RoleCRUD(auth).get(name=data.name)
|
||||||
if exist_role and exist_role.id != id:
|
if exist_role and exist_role.id != id:
|
||||||
raise CustomException(msg="更新失败,角色名称重复")
|
raise CustomException(msg="更新失败,角色名称重复")
|
||||||
|
exist_code = await RoleCRUD(auth).get(code=data.code)
|
||||||
|
if exist_code and exist_code.id != id:
|
||||||
|
raise CustomException(msg="更新失败,角色编码已存在")
|
||||||
updated_role = await RoleCRUD(auth).update(id=id, data=data)
|
updated_role = await RoleCRUD(auth).update(id=id, data=data)
|
||||||
return RoleOutSchema.model_validate(updated_role).model_dump()
|
return RoleOutSchema.model_validate(updated_role).model_dump()
|
||||||
|
|
||||||
|
|||||||
@@ -178,22 +178,45 @@ def mobile_validator(value: str | None) -> str | None:
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def code_validator(value: str | None) -> str | None:
|
def validate_required_code(value: str | None) -> str:
|
||||||
"""
|
"""
|
||||||
编码验证器。
|
必填编码校验:字母开头,总长 2–16,仅含字母/数字/下划线。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- value (str | None): 编码。
|
- value (str | None): 编码。
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- str | None: 验证后的编码。
|
- str: 去空白后的编码。
|
||||||
|
|
||||||
异常:
|
异常:
|
||||||
- CustomException: 编码格式无效时抛出。
|
- ValueError: 为空或格式不合法。
|
||||||
|
"""
|
||||||
|
if value is None or not str(value).strip():
|
||||||
|
raise ValueError("编码不能为空")
|
||||||
|
v = value.strip()
|
||||||
|
if not re.match(r"^[A-Za-z][A-Za-z0-9_]{1,15}$", v):
|
||||||
|
raise ValueError("编码需字母开头,允许字母/数字/下划线,长度2-16")
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def code_validator(value: str | None) -> str | None:
|
||||||
|
"""
|
||||||
|
可选编码验证器(为空则跳过)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- value (str | None): 编码。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str | None: 验证后的编码;未填写时返回 None。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 已填写但格式无效时抛出。
|
||||||
"""
|
"""
|
||||||
if not value:
|
if not value:
|
||||||
return value
|
return value
|
||||||
v = value.strip()
|
v = value.strip()
|
||||||
|
if not v:
|
||||||
|
return None
|
||||||
if not re.match(r"^[A-Za-z][A-Za-z0-9_]{1,15}$", v):
|
if not re.match(r"^[A-Za-z][A-Za-z0-9_]{1,15}$", v):
|
||||||
raise CustomException(
|
raise CustomException(
|
||||||
code=RET.ERROR.code,
|
code=RET.ERROR.code,
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export interface DeptPageQuery {
|
|||||||
export interface DeptTable extends BaseType {
|
export interface DeptTable extends BaseType {
|
||||||
name?: string;
|
name?: string;
|
||||||
order?: number;
|
order?: number;
|
||||||
code?: string;
|
code: string;
|
||||||
leader?: string;
|
leader?: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
@@ -74,7 +74,7 @@ export interface DeptTable extends BaseType {
|
|||||||
export interface DeptForm extends BaseFormType {
|
export interface DeptForm extends BaseFormType {
|
||||||
name?: string;
|
name?: string;
|
||||||
order?: number;
|
order?: number;
|
||||||
code?: string;
|
code: string;
|
||||||
leader?: string;
|
leader?: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export interface RoleTable extends BaseType {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
order?: number;
|
order?: number;
|
||||||
code?: string;
|
code: string;
|
||||||
data_scope?: number;
|
data_scope?: number;
|
||||||
menus?: permissionMenuType[];
|
menus?: permissionMenuType[];
|
||||||
depts?: permissionDeptType[];
|
depts?: permissionDeptType[];
|
||||||
@@ -90,7 +90,7 @@ export interface RoleTable extends BaseType {
|
|||||||
export interface RoleForm extends BaseFormType {
|
export interface RoleForm extends BaseFormType {
|
||||||
name?: string;
|
name?: string;
|
||||||
order?: number;
|
order?: number;
|
||||||
code?: string;
|
code: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface permissionDataType {
|
export interface permissionDataType {
|
||||||
|
|||||||
@@ -214,7 +214,12 @@
|
|||||||
<el-input v-model="formData.name" placeholder="请输入部门名称" :maxlength="50" />
|
<el-input v-model="formData.name" placeholder="请输入部门名称" :maxlength="50" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="部门编码" prop="code">
|
<el-form-item label="部门编码" prop="code">
|
||||||
<el-input v-model="formData.code" placeholder="请输入部门编码" :maxlength="50" />
|
<el-input
|
||||||
|
v-model="formData.code"
|
||||||
|
placeholder="字母开头,2-16位字母/数字/下划线"
|
||||||
|
:maxlength="16"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="上级部门" prop="parent_id">
|
<el-form-item label="上级部门" prop="parent_id">
|
||||||
<el-tree-select
|
<el-tree-select
|
||||||
@@ -379,12 +384,12 @@ const contentConfig = reactive<IContentConfig<DeptPageQuery>>({
|
|||||||
|
|
||||||
const deptOptions = ref<OptionType[]>([]);
|
const deptOptions = ref<OptionType[]>([]);
|
||||||
|
|
||||||
const detailFormData = ref<DeptTable>({});
|
const detailFormData = ref<DeptTable>({ code: "" });
|
||||||
|
|
||||||
const formData = reactive<DeptForm>({
|
const formData = reactive<DeptForm>({
|
||||||
id: undefined,
|
id: undefined,
|
||||||
name: undefined,
|
name: undefined,
|
||||||
code: undefined,
|
code: "",
|
||||||
order: 1,
|
order: 1,
|
||||||
parent_id: undefined,
|
parent_id: undefined,
|
||||||
status: "0",
|
status: "0",
|
||||||
@@ -397,8 +402,18 @@ const dialogVisible = reactive({
|
|||||||
type: "create" as "create" | "update" | "detail",
|
type: "create" as "create" | "update" | "detail",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const CODE_PATTERN = /^[A-Za-z][A-Za-z0-9_]{1,15}$/;
|
||||||
|
|
||||||
const rules = reactive({
|
const rules = reactive({
|
||||||
name: [{ required: true, message: "请输入部门名称", trigger: "blur" }],
|
name: [{ required: true, message: "请输入部门名称", trigger: "blur" }],
|
||||||
|
code: [
|
||||||
|
{ required: true, message: "请输入部门编码", trigger: "blur" },
|
||||||
|
{
|
||||||
|
pattern: CODE_PATTERN,
|
||||||
|
message: "字母开头,2-16位字母/数字/下划线",
|
||||||
|
trigger: "blur",
|
||||||
|
},
|
||||||
|
],
|
||||||
order: [{ required: true, message: "请输入排序", trigger: "blur" }],
|
order: [{ required: true, message: "请输入排序", trigger: "blur" }],
|
||||||
status: [{ required: true, message: "请选择状态", trigger: "blur" }],
|
status: [{ required: true, message: "请选择状态", trigger: "blur" }],
|
||||||
});
|
});
|
||||||
@@ -406,7 +421,7 @@ const rules = reactive({
|
|||||||
const initialFormData: DeptForm = {
|
const initialFormData: DeptForm = {
|
||||||
id: undefined,
|
id: undefined,
|
||||||
name: undefined,
|
name: undefined,
|
||||||
code: undefined,
|
code: "",
|
||||||
order: 1,
|
order: 1,
|
||||||
parent_id: undefined,
|
parent_id: undefined,
|
||||||
status: "0",
|
status: "0",
|
||||||
|
|||||||
@@ -313,7 +313,12 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="角色编码" prop="code">
|
<el-form-item label="角色编码" prop="code">
|
||||||
<el-input v-model="formData.code" placeholder="请输入角色编码" />
|
<el-input
|
||||||
|
v-model="formData.code"
|
||||||
|
placeholder="字母开头,2-16位字母/数字/下划线"
|
||||||
|
maxlength="16"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="状态" prop="status">
|
<el-form-item label="状态" prop="status">
|
||||||
@@ -552,7 +557,7 @@ const formData = reactive<RoleForm>({
|
|||||||
id: undefined,
|
id: undefined,
|
||||||
name: undefined,
|
name: undefined,
|
||||||
order: 1,
|
order: 1,
|
||||||
code: undefined,
|
code: "",
|
||||||
status: "0",
|
status: "0",
|
||||||
description: undefined,
|
description: undefined,
|
||||||
});
|
});
|
||||||
@@ -563,8 +568,18 @@ const dialogVisible = reactive({
|
|||||||
type: "create" as "create" | "update" | "detail",
|
type: "create" as "create" | "update" | "detail",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const CODE_PATTERN = /^[A-Za-z][A-Za-z0-9_]{1,15}$/;
|
||||||
|
|
||||||
const rules = reactive({
|
const rules = reactive({
|
||||||
name: [{ required: true, message: "请输入角色名称", trigger: "blur" }],
|
name: [{ required: true, message: "请输入角色名称", trigger: "blur" }],
|
||||||
|
code: [
|
||||||
|
{ required: true, message: "请输入角色编码", trigger: "blur" },
|
||||||
|
{
|
||||||
|
pattern: CODE_PATTERN,
|
||||||
|
message: "字母开头,2-16位字母/数字/下划线",
|
||||||
|
trigger: "blur",
|
||||||
|
},
|
||||||
|
],
|
||||||
order: [{ required: true, message: "请输入角色排序", trigger: "blur" }],
|
order: [{ required: true, message: "请输入角色排序", trigger: "blur" }],
|
||||||
status: [{ required: true, message: "请选择状态", trigger: "blur" }],
|
status: [{ required: true, message: "请选择状态", trigger: "blur" }],
|
||||||
});
|
});
|
||||||
@@ -573,7 +588,7 @@ const initialFormData: RoleForm = {
|
|||||||
id: undefined,
|
id: undefined,
|
||||||
name: undefined,
|
name: undefined,
|
||||||
order: 1,
|
order: 1,
|
||||||
code: undefined,
|
code: "",
|
||||||
status: "0",
|
status: "0",
|
||||||
description: undefined,
|
description: undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<!-- 租户管理(占位页:后端菜单若指向该组件路径,需在此实现完整 CRUD) -->
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-empty description="租户管理页面尚未实现" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: "Tenant" });
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user