model.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. # -*- coding: utf-8 -*-
  2. # @Author : YY
  3. from datetime import datetime
  4. from io import BytesIO
  5. from threading import Lock
  6. from types import NoneType
  7. from typing import Any, Dict, Generator, Iterator, List, Literal, Optional, Set, Tuple, Union
  8. from flask import g
  9. from typing_extensions import Annotated
  10. from dataclasses import dataclass, field, replace
  11. from werkzeug.datastructures import FileStorage, ImmutableMultiDict
  12. from sqlalchemy import Row
  13. from flask_sqlalchemy.model import Model
  14. from pydantic.alias_generators import to_camel,to_pascal
  15. from pydantic.aliases import AliasGenerator
  16. from pydantic.fields import FieldInfo
  17. from pydantic import AliasChoices, AliasPath, BaseModel, BeforeValidator, \
  18. ConfigDict, Field, ValidationInfo, computed_field, field_validator, model_validator
  19. from ruoyi_common.base.schema_excel import ExcelAccess
  20. from ruoyi_common.base.transformer import to_datetime
  21. from ruoyi_common.constant import HttpStatus
  22. from ruoyi_common.utils.base import DateUtil
  23. strict_base_config = ConfigDict(
  24. from_attributes = True,
  25. alias_generator = to_camel,
  26. frozen = False,
  27. extra = "forbid",
  28. strict = True,
  29. populate_by_name = True,
  30. json_encoders = {
  31. datetime: lambda v: v.strftime(DateUtil.YYYY_MM_DD_HH_MM_SS)
  32. },
  33. )
  34. general_response_serial_config = ConfigDict(
  35. from_attributes = True,
  36. alias_generator = to_camel,
  37. extra = "allow",
  38. strict = True,
  39. populate_by_name = True,
  40. frozen = False,
  41. json_encoders = {
  42. datetime: lambda v: v.strftime(DateUtil.YYYY_MM_DD_HH_MM_SS)
  43. },
  44. )
  45. @dataclass
  46. class ExtraOpt:
  47. name:str = field(init=False)
  48. info:FieldInfo = field(init=False)
  49. @dataclass
  50. class BetOpt(ExtraOpt):
  51. min:str = None
  52. max:str = None
  53. active:Literal["min","max","default"] = "default"
  54. def replace(self, **kwargs):
  55. return replace(self, **kwargs)
  56. @dataclass(frozen=True)
  57. class VoAccess:
  58. body: bool = True
  59. query: Union[ExtraOpt,bool] = False
  60. sort: bool = False
  61. @dataclass
  62. class VoValidatorContext:
  63. is_page: bool = False
  64. is_sort: bool = False
  65. exclude_data_alias: bool = False
  66. include_sort_alias: Set = field(default_factory=set)
  67. include_fields: Set = field(default_factory=set)
  68. exclude_fields: Set = field(default_factory=set)
  69. @dataclass
  70. class DbValidatorContext:
  71. col_entity_list: List[Any]
  72. @dataclass
  73. class VoSerializerContext:
  74. exclude_fields: Set = field(default_factory=set)
  75. include_fields: Set = field(default_factory=set)
  76. by_alias: bool = True
  77. exclude_none: bool = True
  78. exclude_unset: bool = True
  79. exclude_default: bool = False
  80. is_excel: bool = False
  81. def as_kwargs(self):
  82. return {
  83. "by_alias": self.by_alias,
  84. "exclude": self.exclude_fields,
  85. "include": self.include_fields,
  86. "exclude_none": self.exclude_none,
  87. "exclude_unset": self.exclude_unset,
  88. "exclude_defaults": self.exclude_default
  89. }
  90. @dataclass
  91. class CriterianMeta:
  92. _scope: List = field(default_factory=list)
  93. _page: "PageModel" = field(default=None)
  94. _sort: "OrderModel" = field(default=None)
  95. _extra: "ExtraModel" = field(default=None)
  96. @property
  97. def scope(self):
  98. return self._scope
  99. @scope.setter
  100. def scope(self, value):
  101. self._scope = value
  102. @property
  103. def page(self):
  104. return self._page
  105. @page.setter
  106. def page(self, value):
  107. self._page = value
  108. @property
  109. def sort(self):
  110. return self._sort
  111. @sort.setter
  112. def sort(self, value):
  113. self._sort = value
  114. @property
  115. def extra(self):
  116. return self._extra
  117. @extra.setter
  118. def extra(self, value):
  119. self._extra = value
  120. class BaseEntity(BaseModel):
  121. model_config = strict_base_config.copy()
  122. @model_validator(mode="before")
  123. def model_before_validation(cls, data:Any, info:ValidationInfo) -> Dict:
  124. '''
  125. 数据校验前的处理
  126. '''
  127. new_values = {}
  128. if isinstance(info.context,DbValidatorContext):
  129. db_columns_alias = info.context.col_entity_list
  130. if db_columns_alias and db_columns_alias._alia_prefix:
  131. if isinstance(data, Row):
  132. for k in data._mapping:
  133. v = data._mapping[k]
  134. if db_columns_alias.check_prefix(k):
  135. key = db_columns_alias.to_field(k)
  136. new_values[key] = v
  137. else:
  138. continue
  139. else:
  140. if isinstance(data, Row):
  141. new_values = data._mapping
  142. elif isinstance(info.context,VoValidatorContext):
  143. pass
  144. else:
  145. if isinstance(data, Row):
  146. new_values = data._mapping
  147. return new_values if new_values else data
  148. @classmethod
  149. def generate_excel_schema(cls) -> Generator[Tuple[str,ExcelAccess],None,None]:
  150. '''
  151. 生成excel的schema
  152. '''
  153. for k,info in cls.model_fields.items():
  154. if info.json_schema_extra is None:continue
  155. excel_access = info.json_schema_extra.get("excel_access",False)
  156. if excel_access:
  157. if isinstance(excel_access,(list,tuple,)):
  158. for access in excel_access:
  159. yield "{}.{}".format(k,access.attr),access
  160. else:
  161. yield k,excel_access
  162. @classmethod
  163. def rebuild_excel_schema(cls,row:Dict[str,str]) -> Dict[str,str]:
  164. '''
  165. 重新修改excel的schema,将别名修改为实际字段名
  166. '''
  167. new_row = {}
  168. for k,access in cls.generate_excel_schema():
  169. val = row.get(access.name)
  170. if "." in k:
  171. k1,k2 = k.split(".")
  172. new_row.setdefault(k1,{})[k2] = val
  173. else:
  174. new_row[k] = val
  175. return new_row
  176. def generate_excel_data(self) -> Generator[Tuple[str,ExcelAccess],None,None]:
  177. '''
  178. 生成excel数据
  179. '''
  180. data = self.model_dump()
  181. for k,access in self.generate_excel_schema():
  182. if "." in k:
  183. k1,k2 = k.split(".")
  184. sub_data = data.get(k1,{})
  185. if sub_data:
  186. val = sub_data.get(k2,None)
  187. else:
  188. val = None
  189. else:
  190. val = data.get(k)
  191. access.val = val
  192. yield k,access
  193. def create_by_user(self, name: str ) -> None:
  194. self.create_by = name
  195. self.create_time = datetime.now()
  196. def update_by_user(self, name: str) -> None:
  197. self.update_by = name
  198. self.update_time = datetime.now()
  199. class AuditEntity(BaseEntity):
  200. # 创建者
  201. create_by: Annotated[
  202. str | int | NoneType,
  203. Field(default=None,vo=VoAccess(body=False,query=False))
  204. ]
  205. # 创建时间
  206. create_time: Annotated[
  207. Optional[datetime],
  208. BeforeValidator(to_datetime()),
  209. Field(default=None,vo=VoAccess(body=False,query=False))
  210. ]
  211. # 更新者
  212. update_by: Annotated[
  213. str | int | NoneType,
  214. Field(default=None,vo=VoAccess(body=False,query=False))
  215. ]
  216. # 更新时间
  217. update_time: Annotated[
  218. Optional[datetime],
  219. BeforeValidator(to_datetime()),
  220. Field(default=None,vo=VoAccess(body=False,query=False))
  221. ]
  222. # 备注
  223. remark: str | NoneType = None
  224. class AjaxResponse(BaseEntity):
  225. model_config = general_response_serial_config.copy()
  226. # 数据状态码
  227. code: Annotated[int, Field(default=HttpStatus.SUCCESS)]
  228. # 提示信息
  229. msg: Annotated[str, Field(default="")]
  230. # 数据
  231. data: Annotated[Any, Field(default=None)]
  232. __pydantic_extra__: Dict[str, Any] = Field(init=False)
  233. @classmethod
  234. def from_success(cls, msg='操作成功', data=""):
  235. return cls(code=HttpStatus.SUCCESS, msg=msg, data=data)
  236. @classmethod
  237. def from_error(cls, msg='操作失败', data=""):
  238. return cls(code=HttpStatus.ERROR, msg=msg, data=data)
  239. class TableResponse(BaseEntity):
  240. model_config = general_response_serial_config.copy()
  241. # 数据状态码
  242. code: Annotated[int, Field(default=HttpStatus.SUCCESS)]
  243. # 提示信息
  244. msg: Annotated[str, Field(default='查询成功')]
  245. # 数据
  246. rows: Annotated[List, BeforeValidator(lambda x: list(x) if isinstance(x, Iterator | map) else x)]
  247. __pydantic_extra__: Dict[str, Any] = Field(init=False)
  248. @computed_field
  249. @property
  250. def total(self) -> int:
  251. page:PageModel = g.criterian_meta.page
  252. if page and page.total:
  253. return page.total
  254. return len(self.rows)
  255. class TreeEntity(AuditEntity):
  256. # 父菜单名称
  257. parent_name: Annotated[str, Field(default=None)]
  258. # 父菜单ID
  259. parent_id: Annotated[int, Field(default=None)]
  260. # 显示顺序
  261. order_num: Annotated[int, Field(default=None)]
  262. # 祖级列表
  263. ancestors: Annotated[str, Field(default=None)]
  264. # 子部门
  265. children: Annotated[List["TreeEntity"], Field(default=None)]
  266. class MultiFile(ImmutableMultiDict[str, FileStorage]):
  267. def one(self) -> FileStorage:
  268. return next(self.values())
  269. @classmethod
  270. def from_obj(cls, obj: ImmutableMultiDict):
  271. """
  272. 从 ImmutableMultiDict 构造 MultiFile
  273. Args:
  274. obj (ImmutableMultiDict): Flask/Werkzeug 提供的 files 对象
  275. Returns:
  276. MultiFile: 包装后的文件字典
  277. """
  278. # 直接用原始对象初始化,避免使用 **kwargs 造成参数不匹配
  279. return cls(obj)
  280. class VoModel(BaseModel):
  281. model_config = ConfigDict(
  282. from_attributes = False,
  283. alias_generator = AliasGenerator(
  284. alias=to_camel,
  285. validation_alias=to_camel,
  286. serialization_alias=to_pascal,
  287. ),
  288. frozen = False,
  289. extra = "forbid",
  290. strict = True,
  291. populate_by_name = False,
  292. )
  293. @model_validator(mode="before")
  294. def model_before_validation(cls, data:Any, info:ValidationInfo) -> Dict:
  295. """
  296. 处理data中的别名
  297. Args:
  298. data (Any): 数据
  299. info (ValidationInfo): 验证信息
  300. Returns:
  301. Dict: 处理后的数据
  302. """
  303. new_data = {}
  304. for k,finfo in cls.model_fields.items():
  305. alias_set = cls.get_validation_alias(k,finfo)
  306. for alias in alias_set:
  307. if alias in data:
  308. if info.context and info.context.exclude_data_alias:
  309. new_data[alias] = data.pop(alias, None)
  310. else:
  311. new_data[alias] = data.get(alias, None)
  312. return new_data
  313. @classmethod
  314. def get_serialization_alias(cls, name:str, info:FieldInfo) -> Set[str]:
  315. """
  316. 获取字段的序列化别名
  317. Args:
  318. name (str): 字段名称
  319. info (FieldInfo): 字段信息
  320. Raises:
  321. Exception: AliasPath不支持
  322. Returns:
  323. Set[str]: 序列化别名集合
  324. """
  325. alias_set = set()
  326. alias = cls.get_alias_from_config(name,False)
  327. if alias:
  328. alias_set.add(alias)
  329. if info.serialization_alias:
  330. alias_set.add(info.serialization_alias)
  331. return alias_set
  332. @classmethod
  333. def get_validation_alias(cls, name:str, info:FieldInfo) -> Set[str]:
  334. """
  335. 获取字段的校验别名
  336. Args:
  337. name (str): 字段名称
  338. info (FieldInfo): 字段信息
  339. Raises:
  340. Exception: AliasPath不支持
  341. Returns:
  342. Set[str]: 别名集合
  343. """
  344. alias_set = set()
  345. alias = cls.get_alias_from_config(name)
  346. if alias:
  347. alias_set = alias_set | alias
  348. if info.validation_alias:
  349. if isinstance(info.validation_alias, str):
  350. alias_set.add(info.validation_alias)
  351. elif isinstance(info.validation_alias, AliasPath):
  352. raise Exception(f"模型{cls.__name__}的字段不支持AliasPath")
  353. elif isinstance(info.validation_alias, AliasChoices):
  354. alias_set = alias_set | \
  355. set(info.validation_alias.choices)
  356. if "populate_by_name" in cls.model_config \
  357. and cls.model_config["populate_by_name"]:
  358. alias_set.add(name)
  359. return alias_set
  360. @classmethod
  361. def get_alias_from_config(cls,name:str,validation=True)-> Optional[Set[str]]:
  362. """
  363. 从配置中获取别名
  364. Args:
  365. name (str): 字段名称
  366. validation (bool, optional): 是否为验证字段. Defaults to True.
  367. Returns:
  368. Optional[Set[str]]: 别名
  369. """
  370. alias_set = set()
  371. if "generate_alias" in cls.model_config and \
  372. cls.model_config["generate_alias"]:
  373. g_alias,v_alias,s_alias = cls.model_config["generate_alias"].\
  374. generate_aliases(name)
  375. if validation:
  376. if g_alias:
  377. alias_set.add(g_alias)
  378. if v_alias:
  379. alias_set.add(v_alias)
  380. else:
  381. if s_alias:
  382. alias_set.add(s_alias)
  383. return alias_set
  384. class PageModel(VoModel):
  385. model_config = ConfigDict(
  386. from_attributes = False,
  387. alias_generator = AliasGenerator(
  388. alias=to_camel,
  389. validation_alias=to_camel,
  390. serialization_alias=to_pascal,
  391. ),
  392. frozen = False,
  393. extra = "forbid",
  394. strict = True,
  395. populate_by_name = False,
  396. )
  397. page_num: Annotated[
  398. int,
  399. BeforeValidator(int),
  400. Field(1, ge=1,frozen=True)
  401. ]
  402. page_size: Annotated[
  403. int,
  404. BeforeValidator(int),
  405. Field(10, ge=1, le=100, frozen=True)
  406. ]
  407. total: Annotated[int, Field(default=None)]
  408. stmt: Annotated[Any, Field(default=None)]
  409. class OrderModel(VoModel):
  410. order_by_column: Annotated[Optional[List[str]],Field(default=None)]
  411. is_asc: Annotated[Literal["asc", "desc"],Field(default="asc")]
  412. @field_validator("order_by_column",mode="before")
  413. def order_by_column_before_validation(cls, value:str, info:ValidationInfo) -> List[str]:
  414. value = value.split(",")
  415. if info.context and isinstance(info.context,VoValidatorContext):
  416. for val in value:
  417. if val not in info.context.include_sort_alias:
  418. raise ValueError(f"排序字段{val},在禁止的模型字段范围内")
  419. return value
  420. class ExtraModel(VoModel):
  421. begin_time: Annotated[
  422. Optional[datetime],
  423. BeforeValidator(to_datetime()),
  424. Field(default=None)
  425. ]
  426. end_time: Annotated[
  427. Optional[datetime],
  428. BeforeValidator(to_datetime()),
  429. Field(default=None)
  430. ]
  431. class ForbiddenExtraModel(VoModel):
  432. def criterians(self,po:Model)-> List[Any]:
  433. """
  434. 构建查询条件
  435. Args:
  436. po (Model): 数据库模型
  437. Returns:
  438. List[Any]: 查询条件
  439. """
  440. criterions = []
  441. for k,info in self.model_fields.items():
  442. val = getattr(self,k,None)
  443. json_extra = info.json_schema_extra
  444. if json_extra and "vo_opt" in json_extra:
  445. vo_opt:ExtraOpt = json_extra["vo_opt"]
  446. column = getattr(po,vo_opt.name,None)
  447. if column:
  448. if isinstance(vo_opt, BetOpt):
  449. if vo_opt.active == "min":
  450. criterion = column >= val
  451. elif vo_opt.active == "max":
  452. criterion = column <= val
  453. else:
  454. criterion = column == val
  455. criterions.append(criterion)
  456. else:
  457. criterions.append(column == val)
  458. return criterions
  459. class AllowedExtraModel(ForbiddenExtraModel):
  460. model_config = ConfigDict(
  461. from_attributes = False,
  462. alias_generator = AliasGenerator(
  463. alias=to_camel,
  464. validation_alias=to_camel,
  465. serialization_alias=to_pascal,
  466. ),
  467. frozen = True,
  468. extra = "allow",
  469. strict = True,
  470. populate_by_name = False,
  471. )