schema.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. # -*- coding: utf-8 -*-
  2. from typing import Literal
  3. from pydantic import BaseModel, ConfigDict, Field, model_validator
  4. from fastapi import Query
  5. from app.core.validator import DateTimeStr
  6. from app.core.validator import menu_request_validator
  7. from app.core.base_schema import BaseSchema
  8. class MenuCreateSchema(BaseModel):
  9. """菜单创建模型"""
  10. name: str = Field(..., max_length=50, description="菜单名称")
  11. type: int = Field(..., ge=1, le=4, description="菜单类型(1:目录 2:菜单 3:按钮 4:外链)")
  12. order: int = Field(..., ge=1, description="显示顺序")
  13. permission: str | None = Field(default=None, max_length=100, description="权限标识")
  14. icon: str | None = Field(default=None, max_length=100, description="菜单图标")
  15. route_name: str | None = Field(default=None, max_length=100, description="路由名称")
  16. route_path: str | None = Field(default=None, max_length=200, description="路由地址")
  17. component_path: str | None = Field(default=None, max_length=255, description="组件路径")
  18. redirect: str | None = Field(default=None, max_length=200, description="重定向地址")
  19. hidden: bool = Field(default=False, description="是否隐藏(True:是 False:否)")
  20. keep_alive: bool = Field(default=True, description="是否缓存(True:是 False:否)")
  21. always_show: bool = Field(default=False, description="是否始终显示(True:是 False:否)")
  22. title: str | None = Field(default=None, max_length=50, description="菜单标题")
  23. params: list[dict[str, str]] | None = Field(default=None, description="路由参数,格式为[{key: string, value: string}]")
  24. affix: bool = Field(default=False, description="是否固定标签页(True:是 False:否)")
  25. parent_id: int | None = Field(default=None, ge=1, description="父菜单ID")
  26. status: str = Field(default="0", description="是否启用(0:启用 1:禁用)")
  27. description: str | None = Field(default=None, max_length=255, description="描述")
  28. @model_validator(mode='before')
  29. @classmethod
  30. def _normalize(cls, values):
  31. if isinstance(values, dict):
  32. # 字符串去空格
  33. for k in ["name", "icon", "permission", "route_name", "route_path", "component_path", "redirect", "title", "description"]:
  34. if k in values and isinstance(values[k], str):
  35. values[k] = values[k].strip() or None if values[k].strip() == "" else values[k].strip()
  36. # 父ID转整型
  37. if "parent_id" in values and isinstance(values["parent_id"], str):
  38. try:
  39. values["parent_id"] = int(values["parent_id"].strip())
  40. except Exception:
  41. pass
  42. # 路由名/路径规范
  43. import re
  44. if "route_name" in values and isinstance(values["route_name"], str):
  45. rn = values["route_name"]
  46. if rn and not re.match(r"^[A-Za-z][A-Za-z0-9_.-]{1,99}$", rn):
  47. raise ValueError("路由名称需字母开头,仅含字母/数字/_ . -")
  48. if "route_path" in values and isinstance(values["route_path"], str):
  49. rp = values["route_path"]
  50. if rp and not rp.startswith("/"):
  51. raise ValueError("路由路径需以 / 开头")
  52. return values
  53. @model_validator(mode='after')
  54. def validate_fields(self):
  55. return menu_request_validator(self)
  56. class MenuUpdateSchema(MenuCreateSchema):
  57. """菜单更新模型"""
  58. parent_name: str | None = Field(default=None, max_length=50, description="父菜单名称")
  59. class MenuOutSchema(MenuCreateSchema, BaseSchema):
  60. """菜单响应模型"""
  61. model_config = ConfigDict(from_attributes=True)
  62. parent_name: str | None = Field(default=None, max_length=50, description="父菜单名称")
  63. class MenuQueryParam:
  64. """菜单管理查询参数"""
  65. def __init__(
  66. self,
  67. name: str | None = Query(None, description="菜单名称"),
  68. route_path: str | None = Query(None, description="路由地址"),
  69. component_path: str | None = Query(None, description="组件路径"),
  70. type: Literal[1,2,3,4] | None = Query(None, description="菜单类型(1:目录 2:菜单 3:按钮 4:外链)"),
  71. permission: str | None = Query(None, description="权限标识"),
  72. status: str | None = Query(None, description="菜单状态(0:启用 1:禁用)"),
  73. created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
  74. ) -> None:
  75. # 模糊查询字段
  76. self.name = ("like", name)
  77. self.route_path = ("like", route_path)
  78. self.component_path = ("like", component_path)
  79. self.permission = ("like", permission)
  80. # 精确查询字段
  81. self.type = type
  82. self.status = status
  83. # 时间范围查询
  84. if created_time and len(created_time) == 2:
  85. self.created_time = ("between", (created_time[0], created_time[1]))