schema.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. from typing import Literal
  2. from fastapi import Query
  3. from pydantic import BaseModel, ConfigDict, Field, model_validator
  4. from app.common.enums import QueueEnum
  5. from app.core.base_schema import BaseSchema
  6. from app.core.validator import DateTimeStr, menu_request_validator
  7. class MenuCreateSchema(BaseModel):
  8. """菜单创建模型"""
  9. name: str = Field(..., max_length=50, description="菜单名称")
  10. type: int = Field(..., ge=1, le=4, description="菜单类型(1:目录 2:菜单 3:按钮 4:外链)")
  11. order: int = Field(..., ge=1, description="显示顺序")
  12. permission: str | None = Field(default=None, max_length=100, description="权限标识")
  13. icon: str | None = Field(default=None, max_length=100, description="菜单图标")
  14. route_name: str | None = Field(default=None, max_length=100, description="路由名称")
  15. route_path: str | None = Field(default=None, max_length=200, description="路由地址")
  16. component_path: str | None = Field(default=None, max_length=255, description="组件路径")
  17. redirect: str | None = Field(default=None, max_length=200, description="重定向地址")
  18. hidden: bool = Field(default=False, description="是否隐藏(True:是 False:否)")
  19. keep_alive: bool = Field(default=True, description="是否缓存(True:是 False:否)")
  20. always_show: bool = Field(default=False, description="是否始终显示(True:是 False:否)")
  21. title: str | None = Field(default=None, max_length=50, description="菜单标题")
  22. params: list[dict[str, str]] | None = Field(
  23. default=None,
  24. description="路由参数,格式为[{key: string, value: string}]",
  25. )
  26. affix: bool = Field(default=False, description="是否固定标签页(True:是 False:否)")
  27. parent_id: int | None = Field(default=None, ge=1, description="父菜单ID")
  28. status: str = Field(default="0", description="是否启用(0:启用 1:禁用)")
  29. description: str | None = Field(default=None, max_length=255, description="描述")
  30. @model_validator(mode="before")
  31. @classmethod
  32. def _normalize(cls, values):
  33. if isinstance(values, dict):
  34. # 字符串去空格
  35. for k in [
  36. "name",
  37. "icon",
  38. "permission",
  39. "route_name",
  40. "route_path",
  41. "component_path",
  42. "redirect",
  43. "title",
  44. "description",
  45. ]:
  46. if k in values and isinstance(values[k], str):
  47. values[k] = (
  48. values[k].strip() or None if values[k].strip() == "" else values[k].strip()
  49. )
  50. # 父ID转整型
  51. if "parent_id" in values and isinstance(values["parent_id"], str):
  52. try:
  53. values["parent_id"] = int(values["parent_id"].strip())
  54. except Exception:
  55. pass
  56. # 路由名/路径规范
  57. if "route_path" in values and isinstance(values["route_path"], str):
  58. rp = values["route_path"]
  59. if rp and not rp.startswith("/"):
  60. raise ValueError("路由路径需以 / 开头")
  61. if "component_path" in values and isinstance(values["component_path"], str):
  62. cp = values["component_path"]
  63. if cp and cp.startswith("/"):
  64. raise ValueError("组件路径不能以 / 开头")
  65. return values
  66. @model_validator(mode="after")
  67. def validate_fields(self):
  68. """
  69. 统一校验菜单请求字段(委托到 `menu_request_validator`)。
  70. 返回:
  71. - MenuCreateSchema: 校验后的同一实例。
  72. 异常:
  73. - CustomException: 字段不满足菜单类型约束时抛出。
  74. """
  75. return menu_request_validator(self)
  76. class MenuUpdateSchema(MenuCreateSchema):
  77. """菜单更新模型"""
  78. parent_name: str | None = Field(default=None, max_length=50, description="父菜单名称")
  79. class MenuOutSchema(MenuCreateSchema, BaseSchema):
  80. """菜单响应模型"""
  81. model_config = ConfigDict(from_attributes=True)
  82. parent_name: str | None = Field(default=None, max_length=50, description="父菜单名称")
  83. class MenuQueryParam:
  84. """菜单管理查询参数"""
  85. def __init__(
  86. self,
  87. name: str | None = Query(None, description="菜单名称"),
  88. route_path: str | None = Query(None, description="路由地址"),
  89. component_path: str | None = Query(None, description="组件路径"),
  90. type: Literal[1, 2, 3, 4] | None = Query(
  91. None, description="菜单类型(1:目录 2:菜单 3:按钮 4:外链)"
  92. ),
  93. permission: str | None = Query(None, description="权限标识"),
  94. description: str | None = Query(None, description="描述"),
  95. status: str | None = Query(None, description="是否启用"),
  96. created_time: list[DateTimeStr] | None = Query(
  97. None,
  98. description="创建时间范围",
  99. examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"],
  100. ),
  101. updated_time: list[DateTimeStr] | None = Query(
  102. None,
  103. description="更新时间范围",
  104. examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"],
  105. ),
  106. created_id: int | None = Query(None, description="创建人"),
  107. updated_id: int | None = Query(None, description="更新人"),
  108. ) -> None:
  109. # 模糊查询字段
  110. self.name = (QueueEnum.like.value, name)
  111. self.route_path = (QueueEnum.like.value, route_path)
  112. self.component_path = (QueueEnum.like.value, component_path)
  113. self.permission = (QueueEnum.like.value, permission)
  114. # 精确查询字段
  115. self.type = type
  116. # 模糊查询字段
  117. if description:
  118. self.description = (QueueEnum.like.value, description)
  119. # 精确查询字段
  120. if status:
  121. self.status = (QueueEnum.eq.value, status)
  122. # 时间范围查询
  123. if created_time and len(created_time) == 2:
  124. self.created_time = (QueueEnum.between.value, (created_time[0], created_time[1]))
  125. if updated_time and len(updated_time) == 2:
  126. self.updated_time = (QueueEnum.between.value, (updated_time[0], updated_time[1]))
  127. # 关联查询字段
  128. if created_id:
  129. self.created_id = (QueueEnum.eq.value, created_id)
  130. if updated_id:
  131. self.updated_id = (QueueEnum.eq.value, updated_id)