Skip to content
Merged

Dev #455

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,5 @@ frontend/app/*

.plan/*
CLAUDE.md
AGENTS.md
AGENTS.md
.codebuddy
3 changes: 2 additions & 1 deletion backend/app/api/v1/module_platform/menu/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class MenuModel(ModelMixin):

__tablename__: str = "platform_menu"
__table_args__: dict[str, str] = {"comment": "平台菜单表"}
__loader_options__: list[str] = ["roles"]
__loader_options__: list[str] = ["roles", "children"]
__permission_strategy__: PermissionFilterStrategy = PermissionFilterStrategy.ROLE_BASED

name: Mapped[str] = mapped_column(String(50), nullable=False, comment="菜单名称")
Expand Down Expand Up @@ -115,6 +115,7 @@ class MenuModel(ModelMixin):
back_populates="parent",
foreign_keys="MenuModel.parent_id",
order_by="MenuModel.order",
lazy="selectin",
)
roles: Mapped[list["RoleModel"]] = relationship(
secondary="sys_role_menus", back_populates="menus", lazy="selectin"
Expand Down
15 changes: 12 additions & 3 deletions backend/app/api/v1/module_platform/menu/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,22 @@ def validate_fields(self):
return menu_request_validator(self)


class MenuOutSchema(MenuCreateSchema, BaseSchema):
"""菜单响应模型"""
class MenuDetailOutSchema(MenuCreateSchema, BaseSchema):
"""菜单详情响应模型(不含 children,用于详情和更新)"""

model_config = ConfigDict(from_attributes=True)

parent_name: str | None = Field(default=None, max_length=50, description="父菜单名称")
children: list["MenuOutSchema"] | None = Field(default=None, description="子菜单列表")


class MenuTreeOutSchema(MenuDetailOutSchema):
"""菜单树形响应模型(含 children,用于树形列表)"""

children: list["MenuTreeOutSchema"] | None = Field(default=None, description="子菜单列表")


# 兼容旧代码的别名(后续可逐步移除)
MenuOutSchema = MenuDetailOutSchema


class MenuQueryParam:
Expand Down
41 changes: 23 additions & 18 deletions backend/app/api/v1/module_platform/menu/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
from .crud import MenuCRUD
from .schema import (
MenuCreateSchema,
MenuOutSchema,
MenuDetailOutSchema,
MenuQueryParam,
MenuTreeOutSchema,
MenuUpdateSchema,
)

Expand Down Expand Up @@ -66,7 +67,7 @@ async def _validate_parent_child_client(
raise CustomException(msg="子菜单终端须与父菜单一致(均为 pc 或均为 app)")

@classmethod
async def get_menu_detail_service(cls, auth: AuthSchema, id: int) -> MenuOutSchema:
async def get_menu_detail_service(cls, auth: AuthSchema, id: int) -> MenuDetailOutSchema:
"""
获取菜单详情。

Expand All @@ -75,16 +76,16 @@ async def get_menu_detail_service(cls, auth: AuthSchema, id: int) -> MenuOutSche
- id (int): 菜单ID。

返回:
- dict: 菜单详情对象。
- MenuDetailOutSchema: 菜单详情对象。
"""
menu = await MenuCRUD(auth).get(id=id)
# 创建实例后再设置parent_name属性
menu_out = MenuOutSchema.model_validate(menu)
if menu and menu.parent_id:
menu = await MenuCRUD(auth).get(id=id, preload=["roles"])
if not menu:
raise CustomException(msg="菜单不存在")
menu_out = MenuDetailOutSchema.model_validate(menu)
if menu.parent_id:
parent = await MenuCRUD(auth).get(id=menu.parent_id)
if parent:
menu_out.parent_name = parent.name

return menu_out

@classmethod
Expand All @@ -109,13 +110,13 @@ async def get_menu_tree_service(
menu_list = await MenuCRUD(auth).get_tree_list(
search=search.__dict__, order_by=order_by
)
# 转换为字典列表
menu_dict_list = [MenuOutSchema.model_validate(menu).model_dump() for menu in menu_list]
# 转换为字典列表(使用树形 Schema)
menu_dict_list = [MenuTreeOutSchema.model_validate(menu).model_dump() for menu in menu_list]
# 使用traversal_to_tree构建树形结构
return traversal_to_tree(menu_dict_list)

@classmethod
async def create_menu_service(cls, auth: AuthSchema, data: MenuCreateSchema) -> MenuOutSchema:
async def create_menu_service(cls, auth: AuthSchema, data: MenuCreateSchema) -> MenuDetailOutSchema:
"""
创建菜单。

Expand All @@ -124,7 +125,7 @@ async def create_menu_service(cls, auth: AuthSchema, data: MenuCreateSchema) ->
- data (MenuCreateSchema): 创建参数对象。

返回:
- dict: 创建的菜单对象。
- MenuDetailOutSchema: 创建的菜单对象。
"""
search: dict[str, Any] = {}
if data.title is not None:
Expand All @@ -139,11 +140,10 @@ async def create_menu_service(cls, auth: AuthSchema, data: MenuCreateSchema) ->
await cls._validate_parent_child_client(auth, data.parent_id, data.client)

new_menu = await MenuCRUD(auth).create(data=data)
new_menu_dict = MenuOutSchema.model_validate(new_menu)
return new_menu_dict
return MenuDetailOutSchema.model_validate(new_menu)

@classmethod
async def update_menu_service(cls, auth: AuthSchema, id: int, data: MenuUpdateSchema) -> MenuOutSchema:
async def update_menu_service(cls, auth: AuthSchema, id: int, data: MenuUpdateSchema) -> MenuDetailOutSchema:
"""
更新菜单。

Expand All @@ -153,7 +153,7 @@ async def update_menu_service(cls, auth: AuthSchema, id: int, data: MenuUpdateSc
- data (MenuUpdateSchema): 更新参数对象。

返回:
- dict: 更新的菜单对象。
- MenuDetailOutSchema: 更新的菜单对象。
"""
menu = await MenuCRUD(auth).get(id=id)
if not menu:
Expand All @@ -172,15 +172,20 @@ async def update_menu_service(cls, auth: AuthSchema, id: int, data: MenuUpdateSc
parent_menu = await MenuCRUD(auth).get(id=data.parent_id)
if not parent_menu:
raise CustomException(msg="更新失败,父级菜单不存在")

new_menu = await MenuCRUD(auth).update(id=id, data=data)

if data.status is not None:
await cls.set_menu_available_service(
auth=auth, data=BatchSetAvailable(ids=[id], status=data.status)
)

new_menu_dict = MenuOutSchema.model_validate(new_menu)
return new_menu_dict
menu_out = MenuDetailOutSchema.model_validate(new_menu)
if menu_out.parent_id:
parent = await MenuCRUD(auth).get(id=menu_out.parent_id)
if parent:
menu_out.parent_name = parent.name
return menu_out

@classmethod
async def delete_menu_service(cls, auth: AuthSchema, ids: list[int]) -> None:
Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/v1/module_platform/tenant/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ async def create_service(cls, auth: AuthSchema, data: TenantCreateSchema) -> Ten
"password": PwdUtil.set_password_hash(password=password),
"name": f"{tenant_obj.name}管理员",
"tenant_id": tenant_obj.id,
"status": "0",
"status": 0,
"is_superuser": False,
}
try:
Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/v1/module_system/dept/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class DeptModel(ModelMixin, TenantMixin):

__tablename__: str = "sys_dept"
__table_args__ = (UniqueConstraint("tenant_id", "code"), {"comment": "部门表"})
__loader_options__: list[str] = []
__loader_options__: list[str] = ["children"]
__permission_strategy__: PermissionFilterStrategy = PermissionFilterStrategy.DEPT_BASED

name: Mapped[str] = mapped_column(String(64), nullable=False, comment="部门名称")
Expand Down
15 changes: 12 additions & 3 deletions backend/app/api/v1/module_system/dept/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,22 @@ class DeptUpdateSchema(DeptCreateSchema):
"""部门更新模型"""


class DeptOutSchema(DeptCreateSchema, BaseSchema):
"""部门响应模型"""
class DeptDetailOutSchema(DeptCreateSchema, BaseSchema):
"""部门详情响应模型(不含 children,用于详情和更新)"""

model_config = ConfigDict(from_attributes=True)

parent_name: str | None = Field(default=None, max_length=64, description="父部门名称")
children: list["DeptOutSchema"] | None = Field(default=None, description="子部门列表")


class DeptTreeOutSchema(DeptDetailOutSchema):
"""部门树形响应模型(含 children,用于树形列表)"""

children: list["DeptTreeOutSchema"] | None = Field(default=None, description="子部门列表")


# 兼容旧代码的别名(后续可逐步移除)
DeptOutSchema = DeptDetailOutSchema


class DeptQueryParam:
Expand Down
38 changes: 21 additions & 17 deletions backend/app/api/v1/module_system/dept/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
from .crud import DeptCRUD
from .schema import (
DeptCreateSchema,
DeptOutSchema,
DeptDetailOutSchema,
DeptQueryParam,
DeptTreeOutSchema,
DeptUpdateSchema,
)

Expand All @@ -22,7 +23,7 @@ class DeptService:
"""

@classmethod
async def get_dept_detail_service(cls, auth: AuthSchema, id: int) -> DeptOutSchema:
async def get_dept_detail_service(cls, auth: AuthSchema, id: int) -> DeptDetailOutSchema:
"""
获取部门详情。

Expand All @@ -31,20 +32,17 @@ async def get_dept_detail_service(cls, auth: AuthSchema, id: int) -> DeptOutSche
- id (int): 部门 ID。

返回:
- dict: 部门详情对象。
- DeptDetailOutSchema: 部门详情对象。
"""
dept = await DeptCRUD(auth).get(id=id)
if not dept:
raise CustomException(msg="部门不存在")
# 从列属性构建 dict,避免 Pydantic 访问 ORM 关系触发 async 惰性加载
dept_dict = {c.name: getattr(dept, c.name) for c in dept.__table__.columns}
dept_dict["children"] = None
dept_dict["parent_name"] = None
dept_out = DeptDetailOutSchema.model_validate(dept)
if dept.parent_id:
parent = await DeptCRUD(auth).get(id=dept.parent_id)
if parent:
dept_dict["parent_name"] = parent.name
return DeptOutSchema(**dept_dict)
dept_out.parent_name = parent.name
return dept_out

@classmethod
async def get_dept_tree_service(
Expand All @@ -68,13 +66,13 @@ async def get_dept_tree_service(
dept_list = await DeptCRUD(auth).get_tree_list(
search=search.__dict__ if search else {}, order_by=order_by
)
# 转换为字典列表,tree_list 已通过 selectin 预加载 children
dept_dict_list = [DeptOutSchema.model_validate(dept).model_dump() for dept in dept_list]
# 转换为字典列表(使用树形 Schema),tree_list 已通过 selectin 预加载 children
dept_dict_list = [DeptTreeOutSchema.model_validate(dept).model_dump() for dept in dept_list]
# 仅保留根节点,子树已在 model_dump 中递归序列化
return [d for d in dept_dict_list if d.get("parent_id") is None]

@classmethod
async def create_dept_service(cls, auth: AuthSchema, data: DeptCreateSchema) -> DeptOutSchema:
async def create_dept_service(cls, auth: AuthSchema, data: DeptCreateSchema) -> DeptDetailOutSchema:
"""
创建部门。

Expand All @@ -83,7 +81,7 @@ async def create_dept_service(cls, auth: AuthSchema, data: DeptCreateSchema) ->
- data (DeptCreateSchema): 部门创建对象。

返回:
- dict: 新创建的部门对象。
- DeptDetailOutSchema: 新创建的部门对象。

异常:
- CustomException: 当部门已存在时抛出。
Expand All @@ -100,10 +98,10 @@ async def create_dept_service(cls, auth: AuthSchema, data: DeptCreateSchema) ->
await TenantService.check_quota_service(auth, auth.tenant_id, "dept")

dept = await DeptCRUD(auth).create(data=data)
return DeptOutSchema.model_validate(dept)
return DeptDetailOutSchema.model_validate(dept)

@classmethod
async def update_dept_service(cls, auth: AuthSchema, id: int, data: DeptUpdateSchema) -> DeptOutSchema:
async def update_dept_service(cls, auth: AuthSchema, id: int, data: DeptUpdateSchema) -> DeptDetailOutSchema:
"""
更新部门。

Expand All @@ -113,7 +111,7 @@ async def update_dept_service(cls, auth: AuthSchema, id: int, data: DeptUpdateSc
- data (DeptUpdateSchema): 部门更新对象。

返回:
- dict: 更新后的部门对象。
- DeptDetailOutSchema: 更新后的部门对象。

异常:
- CustomException: 当部门不存在或名称重复时抛出。
Expand All @@ -127,8 +125,14 @@ async def update_dept_service(cls, auth: AuthSchema, id: int, data: DeptUpdateSc
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)
return DeptOutSchema.model_validate(dept)
dept_out = DeptDetailOutSchema.model_validate(dept)
if dept_out.parent_id:
parent = await DeptCRUD(auth).get(id=dept_out.parent_id)
if parent:
dept_out.parent_name = parent.name
return dept_out

@classmethod
async def delete_dept_service(cls, auth: AuthSchema, ids: list[int]) -> None:
Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/v1/module_system/notice/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ async def get_notice_available_page_service(cls, auth: AuthSchema) -> dict:
offset=0,
limit=10,
order_by=[{"id": "asc"}],
search={"status": "0"},
search={"status": 0},
out_schema=NoticeOutSchema,
)

Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/v1/module_system/position/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ async def export_position_list_service(cls, position_list: list[dict]) -> bytes:
# 复制数据并转换状态
data = position_list.copy()
for item in data:
item["status"] = "启用" if item.get("status") == "0" else "停用"
item["status"] = "启用" if item.get("status") == 0 else "停用"
item["creator"] = (
item.get("created_by", {}).get("name", "未知")
if isinstance(item.get("created_by"), dict)
Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/v1/module_system/role/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ async def export_role_list_service(cls, role_list: list[dict[str, Any]]) -> byte
# 处理数据
data = role_list.copy()
for item in data:
item["status"] = "启用" if item.get("status") == "0" else "停用"
item["status"] = "启用" if item.get("status") == 0 else "停用"
item["data_scope"] = data_scope_map.get(item.get("data_scope", 1), "")

return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
29 changes: 27 additions & 2 deletions backend/app/api/v1/module_system/user/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,12 +234,37 @@ def validate_password(cls, value: str | None):
return value


class UserUpdateSchema(UserCreateSchema):
class UserUpdateSchema(CurrentUserUpdateSchema):
"""更新"""

model_config = ConfigDict(from_attributes=True)

last_login: DateTimeStr | None = Field(default=None, description="最后登录时间")
username: str | None = Field(default=None, max_length=32, description="用户名")
status: int | None = Field(default=None, ge=0, le=1, description="状态(0:正常 1:禁用)")
description: str | None = Field(default=None, max_length=255, description="备注")
dept_id: int | None = Field(default=None, description="部门ID")
role_ids: list[int] | None = Field(default=[], description="角色ID列表")
position_ids: list[int] | None = Field(default=[], description="岗位ID列表")

@field_validator("status")
@classmethod
def validate_status(cls, value: int | None):
"""校验状态:仅支持 0(正常)、1(禁用)"""
if value is not None and value not in {0, 1}:
raise ValueError("状态仅支持 0(正常) 或 1(禁用)")
return value

@field_validator("username")
@classmethod
def validate_username(cls, value: str | None):
"""校验账号:字母开头,2-32 位"""
if not value:
return value
v = value.strip()
import re
if not re.match(r"^[A-Za-z][A-Za-z0-9_.-]{1,31}$", v):
raise ValueError("账号需以字母开头,2-32 位,仅允许字母、数字、_ . -")
return v


class UserOutSchema(UserUpdateSchema, BaseSchema, UserBySchema, TenantBySchema):
Expand Down
Loading