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:
zhangtao
2026-04-04 02:16:53 +08:00
parent 8397c305bd
commit 1255729cd4
13 changed files with 103 additions and 43 deletions
@@ -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()
+27 -4
View File
@@ -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,
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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>