schema.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. import re
  2. from dataclasses import dataclass
  3. from fastapi import Query
  4. from pydantic import BaseModel, ConfigDict, Field, field_validator
  5. from app.common.enums import QueueEnum
  6. from app.core.base_schema import BaseSchema
  7. class GenDBTableSchema(BaseModel):
  8. """数据库中的表信息(跨方言统一结构)。
  9. - 供“导入表结构”与“同步结构”环节使用。
  10. """
  11. model_config = ConfigDict(from_attributes=True)
  12. database_name: str | None = Field(default=None, description="数据库名称")
  13. table_name: str | None = Field(default=None, description="表名称")
  14. table_type: str | None = Field(default=None, description="表类型")
  15. table_comment: str | None = Field(default=None, description="表描述")
  16. class GenCreateTableSqlBody(BaseModel):
  17. """从代码生成页提交的建表 SQL(JSON 对象,便于与前端 axios 一致)。"""
  18. sql: str = Field(..., description="CREATE TABLE 等 DDL,可多条语句")
  19. class GenTableColumnSchema(BaseModel):
  20. """代码生成业务表字段创建模型(原始字段+生成配置)。
  21. - 从根本上解决问题:所有字段都设置了合理的默认值,避免None值问题
  22. """
  23. model_config = ConfigDict(from_attributes=True)
  24. table_id: int = Field(default=0, description="归属表编号")
  25. column_name: str = Field(default="", description="列名称")
  26. column_comment: str | None = Field(default="", description="列描述")
  27. column_type: str = Field(default="varchar(255)", description="列类型")
  28. column_length: str | None = Field(default="", description="列长度")
  29. column_default: str | None = Field(default="", description="列默认值")
  30. is_pk: bool = Field(default=False, description="是否主键(True是 False否)")
  31. is_increment: bool = Field(default=False, description="是否自增(True是 False否)")
  32. is_nullable: bool = Field(default=True, description="是否允许为空(True是 False否)")
  33. is_unique: bool = Field(default=False, description="是否唯一(True是 False否)")
  34. python_type: str = Field(default="str", description="python类型")
  35. python_field: str = Field(default="", description="python字段名")
  36. # 这些开关若默认 True,会导致导入/同步时无法触发自动推断(如主键 id 误入新增/编辑/列表/查询)
  37. # 约定:None 表示“未配置”,由 GenUtils.init_column_field 推断填充
  38. is_insert: bool | None = Field(default=None, description="是否为新增字段(True是 False否)")
  39. is_edit: bool | None = Field(default=None, description="是否编辑字段(True是 False否)")
  40. is_list: bool | None = Field(default=None, description="是否列表字段(True是 False否)")
  41. is_query: bool | None = Field(default=None, description="是否查询字段(True是 False否)")
  42. query_type: str | None = Field(
  43. default=None, description="查询方式(等于、不等于、大于、小于、范围)"
  44. )
  45. # html_type 若给默认值会导致导入/同步时无法触发自动推断(全部变成 input)
  46. # 约定:None 表示“未配置”,由 GenUtils.init_column_field 推断填充
  47. html_type: str | None = Field(
  48. default=None,
  49. description="显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)",
  50. )
  51. dict_type: str | None = Field(default="", description="字典类型")
  52. sort: int = Field(default=0, description="排序")
  53. class GenTableColumnOutSchema(GenTableColumnSchema, BaseSchema):
  54. """
  55. 业务表字段输出模型
  56. """
  57. model_config = ConfigDict(from_attributes=True)
  58. super_column: str | None = Field(default="0", description="是否为基类字段(1是 0否)")
  59. class GenTableSchema(BaseModel):
  60. """代码生成业务表更新模型(扩展聚合字段)。
  61. - 聚合:`columns`字段包含字段列表;`pk_column`主键字段;子表结构`sub_table`。
  62. """
  63. """代码生成业务表基础模型(创建/更新共享字段)。
  64. - 说明:`params`为前端结构体,后端持久化为`options`的JSON。
  65. """
  66. model_config = ConfigDict(from_attributes=True)
  67. table_name: str = Field(..., description="表名称")
  68. table_comment: str | None = Field(default=None, description="表描述")
  69. class_name: str | None = Field(default=None, description="实体类名称")
  70. package_name: str | None = Field(default=None, description="生成包路径")
  71. module_name: str | None = Field(default=None, description="生成模块名")
  72. business_name: str | None = Field(
  73. default=None,
  74. description=(
  75. "功能子目录/路由段;导入时默认表名;同 module_name 下多表须不同。"
  76. "可含斜杠表示嵌套,参考 module_example:demo、demo/demo01、gen_demo02。"
  77. ),
  78. )
  79. function_name: str | None = Field(default=None, description="生成功能名")
  80. sub_table_name: str | None = Field(default=None, description="关联子表的表名")
  81. sub_table_fk_name: str | None = Field(default=None, description="子表关联的外键名")
  82. parent_menu_id: int | None = Field(
  83. default=None,
  84. description=(
  85. "写入本地须选目录类型。有值:侧栏 上级/短包名/功能/按钮,页面路由 /包名/业务名。"
  86. "留空:侧栏 module_包名/功能/按钮,页面路由 /module_包名/业务名(与 plugin 一致);"
  87. "后端 HTTP 接口仍为 /短名(module_xxx→/xxx)。"
  88. ),
  89. )
  90. description: str | None = Field(default=None, max_length=255, description="描述")
  91. columns: list["GenTableColumnOutSchema"] | None = Field(default=None, description="表列信息")
  92. @staticmethod
  93. def _normalize_slug_segment(v: str, *, allow_slash: bool = False) -> str:
  94. """
  95. 将输入规范成工程约定的路径片段:
  96. - 小写
  97. - 只保留 a-z 0-9 _
  98. - 连续 _ 合并
  99. - 首字符不能是数字(则前置 _)
  100. - allow_slash=True 时允许多段 path(每段分别规范)
  101. """
  102. raw = (v or "").strip()
  103. if not raw:
  104. return ""
  105. if allow_slash:
  106. segs = [s for s in raw.strip("/").split("/") if s.strip()]
  107. norm = [GenTableSchema._normalize_slug_segment(s, allow_slash=False) for s in segs]
  108. norm = [s for s in norm if s]
  109. return "/".join(norm)
  110. s = raw.lower()
  111. s = re.sub(r"[^a-z0-9_]+", "_", s)
  112. s = re.sub(r"_+", "_", s).strip("_")
  113. if not s:
  114. return ""
  115. if s[0].isdigit():
  116. s = "_" + s
  117. return s
  118. @field_validator("table_name")
  119. @classmethod
  120. def table_name_update(cls, v: str) -> str:
  121. """
  122. 校验并规范化表名称。
  123. 参数:
  124. - v (str): 原始表名。
  125. 返回:
  126. - str: 去空白后的表名。
  127. 异常:
  128. - ValueError: 表名为空时抛出。
  129. """
  130. if not v:
  131. raise ValueError("表名称不能为空")
  132. return v.strip()
  133. @field_validator(
  134. "table_comment",
  135. "class_name",
  136. "function_name",
  137. "description",
  138. mode="before",
  139. )
  140. @classmethod
  141. def strip_optional_text_fields(cls, v: str | None) -> str | None:
  142. """
  143. 文本类字段统一去首尾空格;空串视为 None。
  144. 参数:
  145. - v (str | None): 原始值。
  146. 返回:
  147. - str | None: 非空字符串或 None。
  148. """
  149. if v is None:
  150. return None
  151. s = str(v).strip()
  152. return s if s else None
  153. @field_validator("package_name", mode="before")
  154. @classmethod
  155. def normalize_package_name(cls, v: str | None) -> str | None:
  156. """
  157. 包名规范:必须是 module_xxx 形态(工程约定)。
  158. 参数:
  159. - v (str | None): 原始包名。
  160. 返回:
  161. - str | None: 规范化后的包名或 None。
  162. """
  163. if v is None:
  164. return None
  165. s = cls._normalize_slug_segment(str(v), allow_slash=False)
  166. if not s:
  167. return None
  168. return s if s.startswith("module_") else f"module_{s}"
  169. @field_validator("module_name", mode="before")
  170. @classmethod
  171. def normalize_module_name(cls, v: str | None) -> str | None:
  172. """
  173. 模块名规范:不带 module_ 前缀;统一按 slug 规范。
  174. 参数:
  175. - v (str | None): 原始模块名。
  176. 返回:
  177. - str | None: 规范化后的模块名或 None。
  178. """
  179. if v is None:
  180. return None
  181. s = cls._normalize_slug_segment(str(v), allow_slash=False)
  182. if not s:
  183. return None
  184. return s[7:] if s.startswith("module_") else s
  185. @field_validator("business_name", mode="before")
  186. @classmethod
  187. def normalize_business_name(cls, v: str | None) -> str | None:
  188. """
  189. 业务名允许多段(如 demo/demo01);统一按 slug 规范。
  190. 参数:
  191. - v (str | None): 原始业务名。
  192. 返回:
  193. - str | None: 规范化后的业务名或 None。
  194. """
  195. if v is None:
  196. return None
  197. s = cls._normalize_slug_segment(str(v), allow_slash=True)
  198. return s if s else None
  199. @field_validator("sub_table_name", "sub_table_fk_name", mode="before")
  200. @classmethod
  201. def strip_optional_sub_fields(cls, v: str | None) -> str | None:
  202. """
  203. 主子表字段去首尾空格,空串视为未填。
  204. 参数:
  205. - v (str | None): 原始值。
  206. 返回:
  207. - str | None: 非空字符串或 None。
  208. """
  209. if v is None:
  210. return None
  211. s = str(v).strip()
  212. return s if s else None
  213. class GenTableOutSchema(GenTableSchema, BaseSchema):
  214. """业务表输出模型(面向控制器/前端)。"""
  215. model_config = ConfigDict(from_attributes=True)
  216. pk_column: GenTableColumnOutSchema | None = Field(default=None, description="主键信息")
  217. # 子表同样需要携带 columns/pk_column 等输出字段,使用 OutSchema 便于模板与类型检查
  218. sub_table: "GenTableOutSchema | None" = Field(default=None, description="子表信息")
  219. sub: bool | None = Field(default=None, description="是否为子表")
  220. master_sub_hint: str | None = Field(
  221. default=None,
  222. description="主子表配置说明或异常提示(仅接口输出,不落库)",
  223. )
  224. class GenSyncColumnChange(BaseModel):
  225. """同步差异:单个字段的变化项(用于预览,不落库)。"""
  226. model_config = ConfigDict(from_attributes=True)
  227. column_name: str = Field(..., description="列名")
  228. change_fields: list[str] = Field(default_factory=list, description="变化字段名列表")
  229. before: dict = Field(default_factory=dict, description="同步前(当前 gen 配置)摘要")
  230. after: dict = Field(default_factory=dict, description="同步后(来自 DB)摘要")
  231. class GenSyncPreviewSchema(BaseModel):
  232. """同步数据库前的差异预览(主表 + 可选子表)。"""
  233. model_config = ConfigDict(from_attributes=True)
  234. table_name: str = Field(..., description="表名")
  235. added: list[str] = Field(default_factory=list, description="新增列(DB有、gen无)")
  236. removed: list[str] = Field(default_factory=list, description="删除列(gen有、DB无)")
  237. changed: list[GenSyncColumnChange] = Field(default_factory=list, description="变更列(同名但属性变化)")
  238. unchanged: int = Field(default=0, description="未变化列数(同名且关键属性一致)")
  239. sub_table_name: str | None = Field(default=None, description="子表表名")
  240. sub: "GenSyncPreviewSchema | None" = Field(default=None, description="子表差异(若配置了主子表)")
  241. @dataclass
  242. class GenTableQueryParam:
  243. """代码生成业务表查询参数
  244. - 支持按`table_name`、`table_comment`进行模糊检索(由CRUD层实现like)。
  245. - 空值将被忽略,不参与过滤。
  246. """
  247. def __init__(
  248. self,
  249. table_name: str | None = Query(None, description="表名称"),
  250. table_comment: str | None = Query(None, description="表注释"),
  251. ) -> None:
  252. # 模糊查询字段
  253. self.table_name = (QueueEnum.like.value, table_name)
  254. self.table_comment = (QueueEnum.like.value, table_comment)
  255. class GenTableColumnQueryParam:
  256. """代码生成业务表字段查询参数
  257. - `column_name`按like规则模糊查询(透传到CRUD层)
  258. """
  259. def __init__(
  260. self,
  261. column_name: str | None = Query(None, description="列名称"),
  262. ) -> None:
  263. # 模糊查询字段:约定("like", 值)格式,便于CRUD解析
  264. self.column_name = (QueueEnum.like.value, column_name)