fred-admin 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (376) hide show
  1. fred_admin/__init__.py +91 -0
  2. fred_admin/common/AliyunSms.py +66 -0
  3. fred_admin/common/Blueprints.py +99 -0
  4. fred_admin/common/Email.py +23 -0
  5. fred_admin/common/Extensions.py +319 -0
  6. fred_admin/common/HandleExcetion.py +204 -0
  7. fred_admin/common/ImageVerification.py +117 -0
  8. fred_admin/common/IoTDBClient.py +698 -0
  9. fred_admin/common/PageSchema.py +65 -0
  10. fred_admin/common/Response.py +251 -0
  11. fred_admin/common/Route.py +390 -0
  12. fred_admin/common/RuntimeHook.py +111 -0
  13. fred_admin/common/SchedulerLog.py +277 -0
  14. fred_admin/common/Sqlacodegen.py +170 -0
  15. fred_admin/common/Swagger.py +77 -0
  16. fred_admin/common/SystemLog.py +80 -0
  17. fred_admin/common/Utils.py +1060 -0
  18. fred_admin/common/WechatLogin.py +97 -0
  19. fred_admin/common/__init__.py +3 -0
  20. fred_admin/config/Config.py +104 -0
  21. fred_admin/config/Logger.py +34 -0
  22. fred_admin/config/__init__.py +3 -0
  23. fred_admin/create_module.py +787 -0
  24. fred_admin/demo/app.py +6 -0
  25. fred_admin/demo/common/__init__.py +1 -0
  26. fred_admin/demo/common/config/Config.py +82 -0
  27. fred_admin/demo/common/config/README.md +8 -0
  28. fred_admin/demo/common/config/__init__.py +1 -0
  29. fred_admin/demo/common/config/__pycache__/Config.cpython-312.pyc +0 -0
  30. fred_admin/demo/common/config/__pycache__/__init__.cpython-312.pyc +0 -0
  31. fred_admin/demo/common/config/jwt_secret_key +1 -0
  32. fred_admin/demo/common/config/session_secret_key +1 -0
  33. fred_admin/demo/common/model/__pycache__/__init__.cpython-312.pyc +0 -0
  34. fred_admin/demo/common/model/__pycache__/model.cpython-312.pyc +0 -0
  35. fred_admin/demo/common/model/model.py +274 -0
  36. fred_admin/demo/demo_.gitignore +42 -0
  37. fred_admin/demo/modules/__init__.py +1 -0
  38. fred_admin/demo/modules/admin/README.md +38 -0
  39. fred_admin/demo/modules/admin/__init__.py +3 -0
  40. fred_admin/demo/modules/admin/backend/__init__.py +7 -0
  41. fred_admin/demo/modules/admin/backend/constant/RedisKey.py +10 -0
  42. fred_admin/demo/modules/admin/backend/controller/AdminAuthController.py +39 -0
  43. fred_admin/demo/modules/admin/backend/controller/AdminController.py +226 -0
  44. fred_admin/demo/modules/admin/backend/controller/ApiAuthController.py +93 -0
  45. fred_admin/demo/modules/admin/backend/controller/ButtonManageController.py +90 -0
  46. fred_admin/demo/modules/admin/backend/controller/FileController.py +28 -0
  47. fred_admin/demo/modules/admin/backend/controller/OrganizationController.py +218 -0
  48. fred_admin/demo/modules/admin/backend/controller/RoleController.py +143 -0
  49. fred_admin/demo/modules/admin/backend/controller/SystemController.py +112 -0
  50. fred_admin/demo/modules/admin/backend/controller/SystemLogController.py +63 -0
  51. fred_admin/demo/modules/admin/backend/controller/TableController.py +175 -0
  52. fred_admin/demo/modules/admin/backend/controller/__init__.py +55 -0
  53. fred_admin/demo/modules/admin/backend/schema/AdminSchema.py +356 -0
  54. fred_admin/demo/modules/admin/backend/schema/ApiAuthSchema.py +72 -0
  55. fred_admin/demo/modules/admin/backend/schema/AuthMenuSchema.py +79 -0
  56. fred_admin/demo/modules/admin/backend/schema/ButtonManageSchema.py +98 -0
  57. fred_admin/demo/modules/admin/backend/schema/OrganizationSchema.py +134 -0
  58. fred_admin/demo/modules/admin/backend/schema/RoleSchema.py +94 -0
  59. fred_admin/demo/modules/admin/backend/schema/SystemConfigSchema.py +115 -0
  60. fred_admin/demo/modules/admin/backend/schema/TableSchema.py +153 -0
  61. fred_admin/demo/modules/admin/backend/schema/UploadImg.py +35 -0
  62. fred_admin/demo/modules/admin/backend/schema/__init__.py +0 -0
  63. fred_admin/demo/modules/admin/backend/schema/__pycache__/AdminSchema.cpython-312.pyc +0 -0
  64. fred_admin/demo/modules/admin/backend/schema/__pycache__/ApiAuthSchema.cpython-312.pyc +0 -0
  65. fred_admin/demo/modules/admin/backend/schema/__pycache__/AuthMenuSchema.cpython-312.pyc +0 -0
  66. fred_admin/demo/modules/admin/backend/schema/__pycache__/ButtonManageSchema.cpython-312.pyc +0 -0
  67. fred_admin/demo/modules/admin/backend/schema/__pycache__/OrganizationSchema.cpython-312.pyc +0 -0
  68. fred_admin/demo/modules/admin/backend/schema/__pycache__/RoleSchema.cpython-312.pyc +0 -0
  69. fred_admin/demo/modules/admin/backend/schema/__pycache__/SystemConfigSchema.cpython-312.pyc +0 -0
  70. fred_admin/demo/modules/admin/backend/schema/__pycache__/TableSchema.cpython-312.pyc +0 -0
  71. fred_admin/demo/modules/admin/backend/schema/__pycache__/UploadImg.cpython-312.pyc +0 -0
  72. fred_admin/demo/modules/admin/backend/schema/__pycache__/__init__.cpython-312.pyc +0 -0
  73. fred_admin/demo/modules/admin/backend/service/AdminService.py +377 -0
  74. fred_admin/demo/modules/admin/backend/service/ApiAuthService.py +237 -0
  75. fred_admin/demo/modules/admin/backend/service/AuthButtonsService.py +155 -0
  76. fred_admin/demo/modules/admin/backend/service/AuthMenuService.py +265 -0
  77. fred_admin/demo/modules/admin/backend/service/ButtonManageService.py +386 -0
  78. fred_admin/demo/modules/admin/backend/service/ButtonManageService.py.broken +113 -0
  79. fred_admin/demo/modules/admin/backend/service/FileService.py +33 -0
  80. fred_admin/demo/modules/admin/backend/service/OrganizationService.py +465 -0
  81. fred_admin/demo/modules/admin/backend/service/RoleService.py +445 -0
  82. fred_admin/demo/modules/admin/backend/service/SystemConfigService.py +89 -0
  83. fred_admin/demo/modules/admin/backend/service/SystemLogService.py +142 -0
  84. fred_admin/demo/modules/admin/backend/service/TableService.py +978 -0
  85. fred_admin/demo/modules/admin/backend/service/__init__.py +1 -0
  86. fred_admin/demo/modules/admin/backend/sql/app.db +0 -0
  87. fred_admin/demo/modules/admin/backend/sql/fred.sql +3780 -0
  88. fred_admin/demo/modules/admin/frontend/LICENSE +21 -0
  89. fred_admin/demo/modules/admin/frontend/build/getEnv.ts +46 -0
  90. fred_admin/demo/modules/admin/frontend/build/plugins.ts +118 -0
  91. fred_admin/demo/modules/admin/frontend/build/proxy.ts +30 -0
  92. fred_admin/demo/modules/admin/frontend/commitlint.config.cjs +162 -0
  93. fred_admin/demo/modules/admin/frontend/demo_editorconfig +15 -0
  94. fred_admin/demo/modules/admin/frontend/demo_env +15 -0
  95. fred_admin/demo/modules/admin/frontend/demo_env.development +21 -0
  96. fred_admin/demo/modules/admin/frontend/demo_env.production +25 -0
  97. fred_admin/demo/modules/admin/frontend/demo_eslintignore +15 -0
  98. fred_admin/demo/modules/admin/frontend/demo_eslintrc.cjs +61 -0
  99. fred_admin/demo/modules/admin/frontend/demo_gitignore +27 -0
  100. fred_admin/demo/modules/admin/frontend/demo_prettierignore +9 -0
  101. fred_admin/demo/modules/admin/frontend/demo_prettierrc.cjs +41 -0
  102. fred_admin/demo/modules/admin/frontend/demo_stylelintignore +4 -0
  103. fred_admin/demo/modules/admin/frontend/demo_stylelintrc.cjs +40 -0
  104. fred_admin/demo/modules/admin/frontend/index.html +105 -0
  105. fred_admin/demo/modules/admin/frontend/lint-staged.config.cjs +8 -0
  106. fred_admin/demo/modules/admin/frontend/package.json +129 -0
  107. fred_admin/demo/modules/admin/frontend/pnpm-lock.yaml +11150 -0
  108. fred_admin/demo/modules/admin/frontend/postcss.config.cjs +5 -0
  109. fred_admin/demo/modules/admin/frontend/public/logo.png +0 -0
  110. fred_admin/demo/modules/admin/frontend/public/vue.svg +1 -0
  111. fred_admin/demo/modules/admin/frontend/src/App.vue +44 -0
  112. fred_admin/demo/modules/admin/frontend/src/api/config/servicePort.ts +2 -0
  113. fred_admin/demo/modules/admin/frontend/src/api/helper/axiosCancel.ts +57 -0
  114. fred_admin/demo/modules/admin/frontend/src/api/helper/checkStatus.ts +43 -0
  115. fred_admin/demo/modules/admin/frontend/src/api/index.ts +167 -0
  116. fred_admin/demo/modules/admin/frontend/src/api/interface/index.ts +179 -0
  117. fred_admin/demo/modules/admin/frontend/src/api/model/adminModel.ts +135 -0
  118. fred_admin/demo/modules/admin/frontend/src/api/model/apiAuthModel.ts +72 -0
  119. fred_admin/demo/modules/admin/frontend/src/api/model/index.ts +24 -0
  120. fred_admin/demo/modules/admin/frontend/src/api/model/loginModel.ts +79 -0
  121. fred_admin/demo/modules/admin/frontend/src/api/model/roleModel.ts +68 -0
  122. fred_admin/demo/modules/admin/frontend/src/api/model/storeModel.ts +80 -0
  123. fred_admin/demo/modules/admin/frontend/src/api/model/systemModel.ts +162 -0
  124. fred_admin/demo/modules/admin/frontend/src/api/model/uploadModel.ts +153 -0
  125. fred_admin/demo/modules/admin/frontend/src/api/modules/admin.ts +76 -0
  126. fred_admin/demo/modules/admin/frontend/src/api/modules/apiAuth.ts +53 -0
  127. fred_admin/demo/modules/admin/frontend/src/api/modules/buttonManage.ts +119 -0
  128. fred_admin/demo/modules/admin/frontend/src/api/modules/dataBase.ts +60 -0
  129. fred_admin/demo/modules/admin/frontend/src/api/modules/login.ts +54 -0
  130. fred_admin/demo/modules/admin/frontend/src/api/modules/organization.ts +123 -0
  131. fred_admin/demo/modules/admin/frontend/src/api/modules/role.ts +61 -0
  132. fred_admin/demo/modules/admin/frontend/src/api/modules/store.ts +303 -0
  133. fred_admin/demo/modules/admin/frontend/src/api/modules/system.ts +125 -0
  134. fred_admin/demo/modules/admin/frontend/src/api/modules/upload.ts +132 -0
  135. fred_admin/demo/modules/admin/frontend/src/api/modules/user.ts +63 -0
  136. fred_admin/demo/modules/admin/frontend/src/assets/fonts/DIN.otf +0 -0
  137. fred_admin/demo/modules/admin/frontend/src/assets/fonts/MetroDF.ttf +0 -0
  138. fred_admin/demo/modules/admin/frontend/src/assets/fonts/YouSheBiaoTiHei.ttf +0 -0
  139. fred_admin/demo/modules/admin/frontend/src/assets/fonts/font.scss +14 -0
  140. fred_admin/demo/modules/admin/frontend/src/assets/iconfont/iconfont.scss +51 -0
  141. fred_admin/demo/modules/admin/frontend/src/assets/iconfont/iconfont.ttf +0 -0
  142. fred_admin/demo/modules/admin/frontend/src/assets/icons/xianxingdaoyu.svg +1 -0
  143. fred_admin/demo/modules/admin/frontend/src/assets/icons/xianxingdiqiu.svg +1 -0
  144. fred_admin/demo/modules/admin/frontend/src/assets/icons/xianxingditu.svg +1 -0
  145. fred_admin/demo/modules/admin/frontend/src/assets/icons/xianxingfanchuan.svg +1 -0
  146. fred_admin/demo/modules/admin/frontend/src/assets/icons/xianxingfeiji.svg +1 -0
  147. fred_admin/demo/modules/admin/frontend/src/assets/icons/xianxinglvhangriji.svg +1 -0
  148. fred_admin/demo/modules/admin/frontend/src/assets/icons/xianxingtianqiyubao.svg +1 -0
  149. fred_admin/demo/modules/admin/frontend/src/assets/icons/xianxingxiangjipaizhao.svg +1 -0
  150. fred_admin/demo/modules/admin/frontend/src/assets/icons/xianxingxiarilengyin.svg +1 -0
  151. fred_admin/demo/modules/admin/frontend/src/assets/icons/xianxingyoulun.svg +1 -0
  152. fred_admin/demo/modules/admin/frontend/src/assets/icons/xianxingzijiayou.svg +1 -0
  153. fred_admin/demo/modules/admin/frontend/src/assets/images/403.png +0 -0
  154. fred_admin/demo/modules/admin/frontend/src/assets/images/404.png +0 -0
  155. fred_admin/demo/modules/admin/frontend/src/assets/images/500.png +0 -0
  156. fred_admin/demo/modules/admin/frontend/src/assets/images/avatar.gif +0 -0
  157. fred_admin/demo/modules/admin/frontend/src/assets/images/login_bg.svg +33 -0
  158. fred_admin/demo/modules/admin/frontend/src/assets/images/login_left.png +0 -0
  159. fred_admin/demo/modules/admin/frontend/src/assets/images/login_left1.png +0 -0
  160. fred_admin/demo/modules/admin/frontend/src/assets/images/login_left2.png +0 -0
  161. fred_admin/demo/modules/admin/frontend/src/assets/images/login_left3.png +0 -0
  162. fred_admin/demo/modules/admin/frontend/src/assets/images/login_left4.png +0 -0
  163. fred_admin/demo/modules/admin/frontend/src/assets/images/login_left5.png +0 -0
  164. fred_admin/demo/modules/admin/frontend/src/assets/images/logo.svg +1 -0
  165. fred_admin/demo/modules/admin/frontend/src/assets/images/msg01.png +0 -0
  166. fred_admin/demo/modules/admin/frontend/src/assets/images/msg02.png +0 -0
  167. fred_admin/demo/modules/admin/frontend/src/assets/images/msg03.png +0 -0
  168. fred_admin/demo/modules/admin/frontend/src/assets/images/msg04.png +0 -0
  169. fred_admin/demo/modules/admin/frontend/src/assets/images/msg05.png +0 -0
  170. fred_admin/demo/modules/admin/frontend/src/assets/images/notData.png +0 -0
  171. fred_admin/demo/modules/admin/frontend/src/assets/images/welcome.png +0 -0
  172. fred_admin/demo/modules/admin/frontend/src/assets/json/authButtonList.json +8 -0
  173. fred_admin/demo/modules/admin/frontend/src/assets/json/authMenuList.json +1075 -0
  174. fred_admin/demo/modules/admin/frontend/src/assets/mock/Easy-Mock-API.zip +0 -0
  175. fred_admin/demo/modules/admin/frontend/src/components/ECharts/config/index.ts +72 -0
  176. fred_admin/demo/modules/admin/frontend/src/components/ECharts/index.vue +104 -0
  177. fred_admin/demo/modules/admin/frontend/src/components/ErrorMessage/403.vue +19 -0
  178. fred_admin/demo/modules/admin/frontend/src/components/ErrorMessage/404.vue +19 -0
  179. fred_admin/demo/modules/admin/frontend/src/components/ErrorMessage/500.vue +19 -0
  180. fred_admin/demo/modules/admin/frontend/src/components/ErrorMessage/index.scss +32 -0
  181. fred_admin/demo/modules/admin/frontend/src/components/Grid/components/GridItem.vue +68 -0
  182. fred_admin/demo/modules/admin/frontend/src/components/Grid/index.vue +167 -0
  183. fred_admin/demo/modules/admin/frontend/src/components/Grid/interface/index.ts +6 -0
  184. fred_admin/demo/modules/admin/frontend/src/components/ImageCaptcha/index.vue +71 -0
  185. fred_admin/demo/modules/admin/frontend/src/components/ImportExcel/index.scss +3 -0
  186. fred_admin/demo/modules/admin/frontend/src/components/ImportExcel/index.vue +151 -0
  187. fred_admin/demo/modules/admin/frontend/src/components/JsonViewer/index.vue +249 -0
  188. fred_admin/demo/modules/admin/frontend/src/components/Loading/fullScreen.ts +45 -0
  189. fred_admin/demo/modules/admin/frontend/src/components/Loading/index.scss +67 -0
  190. fred_admin/demo/modules/admin/frontend/src/components/Loading/index.vue +13 -0
  191. fred_admin/demo/modules/admin/frontend/src/components/ProTable/components/ColSetting.vue +45 -0
  192. fred_admin/demo/modules/admin/frontend/src/components/ProTable/components/Pagination.vue +101 -0
  193. fred_admin/demo/modules/admin/frontend/src/components/ProTable/components/TableColumn.vue +58 -0
  194. fred_admin/demo/modules/admin/frontend/src/components/ProTable/index.vue +348 -0
  195. fred_admin/demo/modules/admin/frontend/src/components/ProTable/interface/index.ts +87 -0
  196. fred_admin/demo/modules/admin/frontend/src/components/SearchForm/components/SearchFormItem.vue +96 -0
  197. fred_admin/demo/modules/admin/frontend/src/components/SearchForm/index.vue +98 -0
  198. fred_admin/demo/modules/admin/frontend/src/components/SelectFilter/index.scss +63 -0
  199. fred_admin/demo/modules/admin/frontend/src/components/SelectFilter/index.vue +110 -0
  200. fred_admin/demo/modules/admin/frontend/src/components/SelectIcon/index.scss +39 -0
  201. fred_admin/demo/modules/admin/frontend/src/components/SelectIcon/index.vue +90 -0
  202. fred_admin/demo/modules/admin/frontend/src/components/StoreFilter/index.vue +356 -0
  203. fred_admin/demo/modules/admin/frontend/src/components/SvgIcon/index.vue +22 -0
  204. fred_admin/demo/modules/admin/frontend/src/components/SwitchDark/index.vue +12 -0
  205. fred_admin/demo/modules/admin/frontend/src/components/TreeFilter/index.scss +44 -0
  206. fred_admin/demo/modules/admin/frontend/src/components/TreeFilter/index.vue +165 -0
  207. fred_admin/demo/modules/admin/frontend/src/components/Upload/Img.vue +304 -0
  208. fred_admin/demo/modules/admin/frontend/src/components/Upload/Imgs.vue +316 -0
  209. fred_admin/demo/modules/admin/frontend/src/components/WangEditor/index.scss +28 -0
  210. fred_admin/demo/modules/admin/frontend/src/components/WangEditor/index.vue +154 -0
  211. fred_admin/demo/modules/admin/frontend/src/config/amapMap.ts +11 -0
  212. fred_admin/demo/modules/admin/frontend/src/config/index.ts +16 -0
  213. fred_admin/demo/modules/admin/frontend/src/config/nprogress.ts +12 -0
  214. fred_admin/demo/modules/admin/frontend/src/directives/index.ts +28 -0
  215. fred_admin/demo/modules/admin/frontend/src/directives/modules/auth.ts +26 -0
  216. fred_admin/demo/modules/admin/frontend/src/directives/modules/copy.ts +37 -0
  217. fred_admin/demo/modules/admin/frontend/src/directives/modules/debounce.ts +31 -0
  218. fred_admin/demo/modules/admin/frontend/src/directives/modules/draggable.ts +49 -0
  219. fred_admin/demo/modules/admin/frontend/src/directives/modules/longpress.ts +49 -0
  220. fred_admin/demo/modules/admin/frontend/src/directives/modules/throttle.ts +41 -0
  221. fred_admin/demo/modules/admin/frontend/src/directives/modules/waterMarker.ts +36 -0
  222. fred_admin/demo/modules/admin/frontend/src/enums/httpEnum.ts +35 -0
  223. fred_admin/demo/modules/admin/frontend/src/hooks/interface/index.ts +32 -0
  224. fred_admin/demo/modules/admin/frontend/src/hooks/useAuthButtons.ts +22 -0
  225. fred_admin/demo/modules/admin/frontend/src/hooks/useDownload.ts +42 -0
  226. fred_admin/demo/modules/admin/frontend/src/hooks/useHandleData.ts +41 -0
  227. fred_admin/demo/modules/admin/frontend/src/hooks/useI18n.ts +51 -0
  228. fred_admin/demo/modules/admin/frontend/src/hooks/useOnline.ts +31 -0
  229. fred_admin/demo/modules/admin/frontend/src/hooks/useSelection.ts +34 -0
  230. fred_admin/demo/modules/admin/frontend/src/hooks/useTable.ts +150 -0
  231. fred_admin/demo/modules/admin/frontend/src/hooks/useTheme.ts +111 -0
  232. fred_admin/demo/modules/admin/frontend/src/hooks/useTime.ts +38 -0
  233. fred_admin/demo/modules/admin/frontend/src/languages/index.ts +32 -0
  234. fred_admin/demo/modules/admin/frontend/src/languages/modules/en.ts +978 -0
  235. fred_admin/demo/modules/admin/frontend/src/languages/modules/zh.ts +996 -0
  236. fred_admin/demo/modules/admin/frontend/src/layouts/LayoutClassic/index.scss +60 -0
  237. fred_admin/demo/modules/admin/frontend/src/layouts/LayoutClassic/index.vue +62 -0
  238. fred_admin/demo/modules/admin/frontend/src/layouts/LayoutColumns/index.scss +95 -0
  239. fred_admin/demo/modules/admin/frontend/src/layouts/LayoutColumns/index.vue +103 -0
  240. fred_admin/demo/modules/admin/frontend/src/layouts/LayoutTransverse/index.scss +60 -0
  241. fred_admin/demo/modules/admin/frontend/src/layouts/LayoutTransverse/index.vue +61 -0
  242. fred_admin/demo/modules/admin/frontend/src/layouts/LayoutVertical/index.scss +48 -0
  243. fred_admin/demo/modules/admin/frontend/src/layouts/LayoutVertical/index.vue +56 -0
  244. fred_admin/demo/modules/admin/frontend/src/layouts/components/Footer/index.scss +11 -0
  245. fred_admin/demo/modules/admin/frontend/src/layouts/components/Footer/index.vue +9 -0
  246. fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/ToolBarLeft.vue +23 -0
  247. fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/ToolBarRight.vue +51 -0
  248. fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/components/AssemblySize.vue +37 -0
  249. fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/components/Avatar.vue +79 -0
  250. fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/components/Breadcrumb.vue +122 -0
  251. fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/components/CollapseIcon.vue +21 -0
  252. fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/components/Fullscreen.vue +25 -0
  253. fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/components/InfoDialog.vue +22 -0
  254. fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/components/Language.vue +38 -0
  255. fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/components/Message.vue +109 -0
  256. fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/components/PasswordDialog.vue +81 -0
  257. fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/components/SearchMenu.vue +183 -0
  258. fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/components/ThemeSetting.vue +12 -0
  259. fred_admin/demo/modules/admin/frontend/src/layouts/components/Main/components/Maximize.vue +39 -0
  260. fred_admin/demo/modules/admin/frontend/src/layouts/components/Main/index.scss +10 -0
  261. fred_admin/demo/modules/admin/frontend/src/layouts/components/Main/index.vue +88 -0
  262. fred_admin/demo/modules/admin/frontend/src/layouts/components/Menu/SubMenu.vue +85 -0
  263. fred_admin/demo/modules/admin/frontend/src/layouts/components/Tabs/components/MoreButton.vue +85 -0
  264. fred_admin/demo/modules/admin/frontend/src/layouts/components/Tabs/index.scss +69 -0
  265. fred_admin/demo/modules/admin/frontend/src/layouts/components/Tabs/index.vue +115 -0
  266. fred_admin/demo/modules/admin/frontend/src/layouts/components/ThemeDrawer/index.scss +137 -0
  267. fred_admin/demo/modules/admin/frontend/src/layouts/components/ThemeDrawer/index.vue +191 -0
  268. fred_admin/demo/modules/admin/frontend/src/layouts/index.vue +42 -0
  269. fred_admin/demo/modules/admin/frontend/src/layouts/indexAsync.vue +46 -0
  270. fred_admin/demo/modules/admin/frontend/src/main.ts +45 -0
  271. fred_admin/demo/modules/admin/frontend/src/routers/index.ts +106 -0
  272. fred_admin/demo/modules/admin/frontend/src/routers/modules/dynamicRouter.ts +99 -0
  273. fred_admin/demo/modules/admin/frontend/src/routers/modules/staticRouter.ts +63 -0
  274. fred_admin/demo/modules/admin/frontend/src/stores/helper/persist.ts +19 -0
  275. fred_admin/demo/modules/admin/frontend/src/stores/index.ts +8 -0
  276. fred_admin/demo/modules/admin/frontend/src/stores/interface/index.ts +63 -0
  277. fred_admin/demo/modules/admin/frontend/src/stores/modules/auth.ts +50 -0
  278. fred_admin/demo/modules/admin/frontend/src/stores/modules/global.ts +55 -0
  279. fred_admin/demo/modules/admin/frontend/src/stores/modules/keepAlive.ts +25 -0
  280. fred_admin/demo/modules/admin/frontend/src/stores/modules/tabs.ts +78 -0
  281. fred_admin/demo/modules/admin/frontend/src/stores/modules/user.ts +23 -0
  282. fred_admin/demo/modules/admin/frontend/src/styles/common.scss +129 -0
  283. fred_admin/demo/modules/admin/frontend/src/styles/element-dark.scss +28 -0
  284. fred_admin/demo/modules/admin/frontend/src/styles/element.scss +264 -0
  285. fred_admin/demo/modules/admin/frontend/src/styles/modules/annotation.scss +746 -0
  286. fred_admin/demo/modules/admin/frontend/src/styles/modules/assembly.scss +714 -0
  287. fred_admin/demo/modules/admin/frontend/src/styles/modules/device-manager.scss +659 -0
  288. fred_admin/demo/modules/admin/frontend/src/styles/modules/directives.scss +381 -0
  289. fred_admin/demo/modules/admin/frontend/src/styles/reset.scss +143 -0
  290. fred_admin/demo/modules/admin/frontend/src/styles/table-optimization.scss +242 -0
  291. fred_admin/demo/modules/admin/frontend/src/styles/theme/aside.ts +16 -0
  292. fred_admin/demo/modules/admin/frontend/src/styles/theme/header.ts +25 -0
  293. fred_admin/demo/modules/admin/frontend/src/styles/theme/menu.ts +31 -0
  294. fred_admin/demo/modules/admin/frontend/src/styles/var.scss +2 -0
  295. fred_admin/demo/modules/admin/frontend/src/typings/global.d.ts +74 -0
  296. fred_admin/demo/modules/admin/frontend/src/typings/utils.d.ts +17 -0
  297. fred_admin/demo/modules/admin/frontend/src/typings/window.d.ts +8 -0
  298. fred_admin/demo/modules/admin/frontend/src/utils/color.ts +59 -0
  299. fred_admin/demo/modules/admin/frontend/src/utils/dict.ts +17 -0
  300. fred_admin/demo/modules/admin/frontend/src/utils/eleValidate.ts +14 -0
  301. fred_admin/demo/modules/admin/frontend/src/utils/errorHandler.ts +28 -0
  302. fred_admin/demo/modules/admin/frontend/src/utils/index.ts +327 -0
  303. fred_admin/demo/modules/admin/frontend/src/utils/is/index.ts +125 -0
  304. fred_admin/demo/modules/admin/frontend/src/utils/mittBus.ts +5 -0
  305. fred_admin/demo/modules/admin/frontend/src/utils/svg.ts +13 -0
  306. fred_admin/demo/modules/admin/frontend/src/views/dataBase/fieldType/components/FieldTypeDrawer.vue +89 -0
  307. fred_admin/demo/modules/admin/frontend/src/views/dataBase/fieldType/index.vue +99 -0
  308. fred_admin/demo/modules/admin/frontend/src/views/dataBase/tableManage/components/DataBaseDrawer.vue +254 -0
  309. fred_admin/demo/modules/admin/frontend/src/views/dataBase/tableManage/components/IndexDrawer.vue +226 -0
  310. fred_admin/demo/modules/admin/frontend/src/views/dataBase/tableManage/components/TableDataDrawer.vue +136 -0
  311. fred_admin/demo/modules/admin/frontend/src/views/dataBase/tableManage/index.vue +234 -0
  312. fred_admin/demo/modules/admin/frontend/src/views/echarts/columnChart/index.scss +0 -0
  313. fred_admin/demo/modules/admin/frontend/src/views/echarts/columnChart/index.vue +139 -0
  314. fred_admin/demo/modules/admin/frontend/src/views/echarts/lineChart/index.scss +0 -0
  315. fred_admin/demo/modules/admin/frontend/src/views/echarts/lineChart/index.vue +123 -0
  316. fred_admin/demo/modules/admin/frontend/src/views/echarts/nestedChart/index.scss +0 -0
  317. fred_admin/demo/modules/admin/frontend/src/views/echarts/nestedChart/index.vue +97 -0
  318. fred_admin/demo/modules/admin/frontend/src/views/echarts/pieChart/index.scss +0 -0
  319. fred_admin/demo/modules/admin/frontend/src/views/echarts/pieChart/index.vue +68 -0
  320. fred_admin/demo/modules/admin/frontend/src/views/echarts/radarChart/index.scss +0 -0
  321. fred_admin/demo/modules/admin/frontend/src/views/echarts/radarChart/index.vue +56 -0
  322. fred_admin/demo/modules/admin/frontend/src/views/echarts/waterChart/index.scss +0 -0
  323. fred_admin/demo/modules/admin/frontend/src/views/echarts/waterChart/index.vue +298 -0
  324. fred_admin/demo/modules/admin/frontend/src/views/home/index.scss +12 -0
  325. fred_admin/demo/modules/admin/frontend/src/views/home/index.vue +11 -0
  326. fred_admin/demo/modules/admin/frontend/src/views/login/components/LoginForm.vue +147 -0
  327. fred_admin/demo/modules/admin/frontend/src/views/login/index.scss +83 -0
  328. fred_admin/demo/modules/admin/frontend/src/views/login/index.vue +27 -0
  329. fred_admin/demo/modules/admin/frontend/src/views/proTable/complexProTable/index.vue +176 -0
  330. fred_admin/demo/modules/admin/frontend/src/views/proTable/components/UserDrawer.vue +152 -0
  331. fred_admin/demo/modules/admin/frontend/src/views/proTable/document/index.vue +13 -0
  332. fred_admin/demo/modules/admin/frontend/src/views/proTable/treeProTable/index.vue +129 -0
  333. fred_admin/demo/modules/admin/frontend/src/views/proTable/useProTable/detail.vue +12 -0
  334. fred_admin/demo/modules/admin/frontend/src/views/proTable/useProTable/index.vue +258 -0
  335. fred_admin/demo/modules/admin/frontend/src/views/proTable/useSelectFilter/index.vue +184 -0
  336. fred_admin/demo/modules/admin/frontend/src/views/proTable/useTreeFilter/detail.vue +12 -0
  337. fred_admin/demo/modules/admin/frontend/src/views/proTable/useTreeFilter/index.vue +147 -0
  338. fred_admin/demo/modules/admin/frontend/src/views/system/accountManage/index.vue +244 -0
  339. fred_admin/demo/modules/admin/frontend/src/views/system/apiAuth/components/ApiDrawer.vue +114 -0
  340. fred_admin/demo/modules/admin/frontend/src/views/system/apiAuth/components/TokenDrawer.vue +160 -0
  341. fred_admin/demo/modules/admin/frontend/src/views/system/apiAuth/index.vue +354 -0
  342. fred_admin/demo/modules/admin/frontend/src/views/system/buttonManage/components/ButtonDrawer.vue +460 -0
  343. fred_admin/demo/modules/admin/frontend/src/views/system/buttonManage/index.vue +426 -0
  344. fred_admin/demo/modules/admin/frontend/src/views/system/configManage/components/ConfigDrawer.vue +106 -0
  345. fred_admin/demo/modules/admin/frontend/src/views/system/configManage/index.vue +116 -0
  346. fred_admin/demo/modules/admin/frontend/src/views/system/departmentManage/index.vue +7 -0
  347. fred_admin/demo/modules/admin/frontend/src/views/system/dictManage/index.vue +7 -0
  348. fred_admin/demo/modules/admin/frontend/src/views/system/menuManage/components/MenuDrawer.vue +262 -0
  349. fred_admin/demo/modules/admin/frontend/src/views/system/menuManage/index.vue +310 -0
  350. fred_admin/demo/modules/admin/frontend/src/views/system/organizationManage/components/CompanyDrawer.vue +72 -0
  351. fred_admin/demo/modules/admin/frontend/src/views/system/organizationManage/components/DepartmentDrawer.vue +120 -0
  352. fred_admin/demo/modules/admin/frontend/src/views/system/organizationManage/components/TeamDrawer.vue +162 -0
  353. fred_admin/demo/modules/admin/frontend/src/views/system/organizationManage/components/TeamMemberDrawer.vue +243 -0
  354. fred_admin/demo/modules/admin/frontend/src/views/system/organizationManage/index.vue +414 -0
  355. fred_admin/demo/modules/admin/frontend/src/views/system/roleManage/components/RoleButtonDrawer.vue +587 -0
  356. fred_admin/demo/modules/admin/frontend/src/views/system/roleManage/components/RoleDrawer.vue +77 -0
  357. fred_admin/demo/modules/admin/frontend/src/views/system/roleManage/components/RoleMenuDrawer.vue +236 -0
  358. fred_admin/demo/modules/admin/frontend/src/views/system/roleManage/components/RoleUserDrawer.vue +125 -0
  359. fred_admin/demo/modules/admin/frontend/src/views/system/roleManage/index.vue +143 -0
  360. fred_admin/demo/modules/admin/frontend/src/views/system/systemLog/components/SystemLogDrawer.vue +207 -0
  361. fred_admin/demo/modules/admin/frontend/src/views/system/systemLog/index.vue +219 -0
  362. fred_admin/demo/modules/admin/frontend/src/views/system/timingTask/index.vue +7 -0
  363. fred_admin/demo/modules/admin/frontend/src/vite-env.d.ts +7 -0
  364. fred_admin/demo/modules/admin/frontend/tsconfig.json +42 -0
  365. fred_admin/demo/modules/admin/frontend/vite.config.ts +91 -0
  366. fred_admin/demo/modules/admin/frontend//321/205/320/231/320/235/321/207/320/273/320/277/321/204/342/225/227/320/263/321/207/320/260/320/221/321/210/320/277/342/224/244/321/206/320/250/320/236.md +100 -0
  367. fred_admin/demo/requirements.txt +1 -0
  368. fred_admin/fonts/NotoSansSC-VariableFont_wght.ttf +0 -0
  369. fred_admin/install_hook.py +1633 -0
  370. fred_admin/setup.py +18 -0
  371. fred_admin-1.0.0.dist-info/METADATA +97 -0
  372. fred_admin-1.0.0.dist-info/RECORD +376 -0
  373. fred_admin-1.0.0.dist-info/WHEEL +5 -0
  374. fred_admin-1.0.0.dist-info/entry_points.txt +3 -0
  375. fred_admin-1.0.0.dist-info/licenses/LICENSE +21 -0
  376. fred_admin-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1060 @@
1
+ import importlib
2
+ import os
3
+ import pkgutil
4
+ import re
5
+ import secrets
6
+ import string
7
+ import hashlib
8
+ import base64
9
+ from datetime import datetime, timedelta
10
+ from decimal import Decimal
11
+ from random import randint
12
+ from time import time
13
+
14
+ from cryptography.fernet import Fernet
15
+ from flask import current_app, request
16
+ from flask_jwt_extended import create_access_token, create_refresh_token
17
+
18
+
19
+ class Utils:
20
+ @staticmethod
21
+ def get_project_root(app=None):
22
+ """
23
+ 获取项目根目录
24
+
25
+ 获取优先级:
26
+ 1. 若提供了 app 且配置了 PROJECT_ROOT,使用该路径
27
+ 2. 若有应用上下文且配置了 PROJECT_ROOT,使用该路径
28
+ 3. 否则使用当前工作目录(Path.cwd()),以适配 run.py 在项目根执行的场景
29
+ """
30
+ from pathlib import Path
31
+
32
+ def _resolve_root(raw):
33
+ if not raw:
34
+ return None
35
+ return Path(raw) if not isinstance(raw, Path) else raw
36
+
37
+ if app is not None:
38
+ project_root = _resolve_root(app.config.get("PROJECT_ROOT"))
39
+ if project_root is not None:
40
+ return project_root
41
+ else:
42
+ try:
43
+ project_root = _resolve_root(current_app.config.get("PROJECT_ROOT"))
44
+ if project_root is not None:
45
+ return project_root
46
+ except RuntimeError:
47
+ pass
48
+ return Path.cwd()
49
+
50
+ @staticmethod
51
+ def get_model_dir(app=None):
52
+ """模型目录名(相对项目根),默认 'common/model'。可由配置 MODEL_DIR 覆盖。"""
53
+ if app is not None:
54
+ return app.config.get("MODEL_DIR", "common/model")
55
+ try:
56
+ return current_app.config.get("MODEL_DIR", "common/model")
57
+ except RuntimeError:
58
+ return "common/model"
59
+
60
+ @staticmethod
61
+ def import_project_models(*model_names, app=None):
62
+ """
63
+ 从项目根目录的 model 目录加载模型模块到 sys.modules
64
+
65
+ 只从项目根目录的 model 目录导入,不会从 fred_admin.model.model 导入
66
+ 使用文件系统路径直接导入,确保不会从已安装的包中导入
67
+ 加载后,可以通过 `from model.model import xxx` 直接使用模型
68
+
69
+ :param model_names: 要验证的模型名称列表(可选,用于验证模型是否存在)
70
+ :param app: Flask 应用实例(可选,用于获取项目根目录)
71
+ """
72
+ import sys
73
+ import importlib.util
74
+ from pathlib import Path
75
+
76
+ # 获取项目根目录与模型目录(支持配置 MODEL_DIR,默认 'model')
77
+ project_root = Utils.get_project_root(app)
78
+ if not isinstance(project_root, Path):
79
+ project_root = Path(project_root)
80
+ model_dir_name = Utils.get_model_dir(app)
81
+ model_file = project_root / model_dir_name / "model.py"
82
+ if not model_file.exists():
83
+ raise ImportError(
84
+ f"无法找到模型文件: {model_file}。"
85
+ f"请配置 SQLALCHEMY_DATABASE_URI 后启动应用,框架将自动生成 {model_dir_name}/model.py。"
86
+ )
87
+
88
+ # 使用文件系统路径直接导入,避免从已安装的包中导入
89
+ try:
90
+ # 生成唯一的模块名,基于项目根目录路径,避免与已安装的包冲突
91
+ module_name = f'_project_model_{hash(str(project_root))}'
92
+
93
+ # 检查是否已经加载过该模块
94
+ if module_name in sys.modules:
95
+ model_module = sys.modules[module_name]
96
+ else:
97
+ # 确保项目根目录在 sys.path 的最前面,避免从已安装的包中导入
98
+ project_root_str = str(project_root)
99
+ if project_root_str in sys.path:
100
+ sys.path.remove(project_root_str)
101
+ sys.path.insert(0, project_root_str)
102
+
103
+ # 临时移除 fred_admin 相关的路径,避免导入已安装的包
104
+ original_path = sys.path.copy()
105
+ filtered_path = [p for p in sys.path if 'fred_admin' not in p or project_root_str in p]
106
+ sys.path[:] = filtered_path
107
+
108
+ try:
109
+ # 从文件路径加载模块
110
+ spec = importlib.util.spec_from_file_location(module_name, model_file)
111
+ if spec is None or spec.loader is None:
112
+ raise ImportError(f"无法从文件路径创建模块规范: {model_file}")
113
+
114
+ model_module = importlib.util.module_from_spec(spec)
115
+ # 设置模块的 __file__ 和 __name__ 属性
116
+ model_module.__file__ = str(model_file)
117
+ model_module.__name__ = module_name
118
+ # 设置 __path__ 为项目根目录,确保相对导入指向正确位置
119
+ model_module.__path__ = [str(project_root / model_dir_name)]
120
+
121
+ sys.modules[module_name] = model_module
122
+ spec.loader.exec_module(model_module)
123
+
124
+ # 同时将模块注册为标准的 model.model 模块,确保所有地方使用同一个模块实例
125
+ # 这样可以避免 "The current Flask app is not registered with this 'SQLAlchemy' instance" 错误
126
+ if 'model.model' not in sys.modules:
127
+ sys.modules['model.model'] = model_module
128
+ finally:
129
+ # 恢复原始 sys.path
130
+ sys.path[:] = original_path
131
+
132
+ # 如果标准的 model.model 模块已存在但不是同一个实例,则同步 db 实例
133
+ if 'model.model' in sys.modules and sys.modules['model.model'] is not model_module:
134
+ # 如果新模块有 db 实例,同步到标准模块
135
+ if hasattr(model_module, 'db'):
136
+ setattr(sys.modules['model.model'], 'db', model_module.db)
137
+
138
+ # 如果提供了模型名称,验证它们是否存在
139
+ if model_names:
140
+ for name in model_names:
141
+ if not hasattr(model_module, name):
142
+ raise AttributeError(f"模型 '{model_file}' 中没有找到 '{name}'")
143
+ except Exception as e:
144
+ error_msg = str(e)
145
+ # 如果错误信息中包含 fred_admin.model.model,提供更清晰的错误提示
146
+ if 'fred_admin.model.model' in error_msg:
147
+ raise ImportError(
148
+ f"无法导入模型模块 '{model_file}'。检测到尝试从已安装的包 'fred_admin.model.model' 导入。"
149
+ f"请确保项目根目录存在 model/model.py 文件,并且该文件不包含从 'fred_admin.model.model' 导入的语句。"
150
+ f"原始错误: {error_msg}"
151
+ )
152
+ raise ImportError(f"无法导入模型模块 '{model_file}'。错误: {error_msg}")
153
+
154
+ @staticmethod
155
+ def reform_decimal(s, num=2) -> float:
156
+ """
157
+ # 四舍五入
158
+ :param s: 小数字符串和 浮点数
159
+ :param num: 保留位数
160
+ :return: 返回浮点数
161
+ """
162
+ if not s:
163
+ return 0.0
164
+ if s != "None":
165
+ result = round(float(s), num)
166
+ return result
167
+ else:
168
+ return s
169
+
170
+ @staticmethod
171
+ def check_type(data) -> str:
172
+ """
173
+ 查询数据类型
174
+ :param data:
175
+ :return: 返回字符串 如果未找到返回unknown
176
+ """
177
+ type_map = {
178
+ int: "int",
179
+ str: "str",
180
+ float: "float",
181
+ list: "list",
182
+ tuple: "tuple",
183
+ dict: "dict",
184
+ set: "set"
185
+ }
186
+ return type_map.get(type(data), "unknown")
187
+
188
+ @staticmethod
189
+ def check_password(pwd: str, min=6, max=20) -> bool:
190
+ """
191
+ 检查密码格式是否正确 大小写和数字特殊符号
192
+ :param pwd: 密码字符串
193
+ :param min: 最小长度
194
+ :param max: 最大长度
195
+ :return: 密码格式是否正确
196
+ """
197
+ match_str = r'^(?=.*[a-z])(?=.*\d)(?=.*[A-Z])[A-Za-z\d#@!~%_^&*!]{'
198
+ r = match_str + str(min) + ',' + str(max) + '}$'
199
+ pattern = re.compile(r)
200
+ return bool(pattern.match(pwd))
201
+
202
+ @staticmethod
203
+ def hash_encrypt(text: str, salt: str = None) -> dict:
204
+ """
205
+ 使用SHA-256算法对给定的密码进行哈希处理,并可以选择性地添加盐值。
206
+
207
+ :param text: 需要哈希的字符串
208
+ :param salt: 可选的盐值,如果未提供,则自动生成
209
+ :return: 包含哈希后的字符串和盐值的字典
210
+ """
211
+ if not text:
212
+ return {}
213
+
214
+ # 如果没有提供盐值,则生成一个随机的4字符长的字符串作为盐值
215
+ if salt is None:
216
+ salt = ''.join(secrets.choice(string.printable) for _ in range(4)) # 生成4个字符的随机字符串
217
+
218
+ # 将盐值和密码组合后进行哈希
219
+ hashed_text = hashlib.sha256(salt.encode('utf-8') + text.encode('utf-8')).hexdigest()
220
+ return {"salt": salt, "hashed_text": hashed_text}
221
+
222
+ @staticmethod
223
+ def generate_secret_key(key_file_path: str) -> None:
224
+ """
225
+ 生成一个安全的 Fernet 密钥,并将其保存在 config 目录下的 fernet_key 文件中。
226
+ """
227
+ # 检查文件是否存在
228
+ if os.path.exists(key_file_path):
229
+ return
230
+ # 确保目录存在
231
+ key_dir = os.path.dirname(key_file_path)
232
+ if key_dir and not os.path.exists(key_dir):
233
+ os.makedirs(key_dir, exist_ok=True)
234
+ # 生成 Fernet 密钥
235
+ key = Fernet.generate_key()
236
+ # 将密钥保存到 config/fernet_key 文件中
237
+ with open(key_file_path, 'w', encoding='utf-8') as key_file:
238
+ key_file.write(key.decode('utf-8'))
239
+
240
+ @staticmethod
241
+ def get_config_dir(app=None):
242
+ """
243
+ 获取配置目录(相对项目根),默认 'common/config'。
244
+ 可由配置 CONFIG_DIR 覆盖。
245
+ """
246
+ if app is not None:
247
+ return app.config.get("CONFIG_DIR", "common/config")
248
+ try:
249
+ return current_app.config.get("CONFIG_DIR", "common/config")
250
+ except RuntimeError:
251
+ return "common/config"
252
+
253
+ @staticmethod
254
+ def get_secret_key(filename: str = 'fernet_key',app=None) -> bytes:
255
+ # 使用相对路径查找config目录,支持自定义 CONFIG_DIR
256
+
257
+ project_root = Utils.get_project_root(app)
258
+ config_dir_name = Utils.get_config_dir(app)
259
+ config_dir = os.path.join(project_root, config_dir_name)
260
+ key_file_path = os.path.join(config_dir, filename)
261
+ if not os.path.exists(key_file_path):
262
+ Utils.generate_secret_key(key_file_path)
263
+ with open(key_file_path, 'r', encoding='utf-8') as key_file:
264
+ key_str = key_file.read()
265
+ key = key_str.encode() # 转换回字节串
266
+ return key
267
+
268
+ @staticmethod
269
+ def fernet_encrypt(text: str) -> str:
270
+ """
271
+ 使用 Fernet 加密文本。 同样的数据 每次加密都不一样
272
+
273
+ :param text: 需要加密的文本 字符串
274
+ :return: 加密后的文本
275
+ """
276
+ key = Utils.get_secret_key()
277
+ cipher = Fernet(key)
278
+ cipher_text = cipher.encrypt(text.encode())
279
+ encrypted_text = base64.b64encode(cipher_text).decode()
280
+ return encrypted_text
281
+
282
+ @staticmethod
283
+ def fernet_decrypt(ciphertext: str) -> str:
284
+ """
285
+ 使用 Fernet 解密文本。
286
+
287
+ :param ciphertext: 加密后的文本
288
+ :return: 解密后的文本
289
+ """
290
+ key = Utils.get_secret_key()
291
+ the_code = base64.b64decode(ciphertext)
292
+ cipher = Fernet(key)
293
+ decrypted_text = cipher.decrypt(the_code).decode()
294
+ return decrypted_text
295
+
296
+ @staticmethod
297
+ def validate_phone(phone_number) -> bool:
298
+ """
299
+ 检查手机格式是否正确。
300
+
301
+ :param phone_number: 手机号码
302
+ :return: 是否符合中国大陆手机号格式
303
+ """
304
+ pattern = r'^1[3-9]\d{9}$'
305
+ return bool(re.match(pattern, phone_number))
306
+
307
+ @staticmethod
308
+ def validate_email(email: str) -> bool:
309
+ """
310
+ 检查邮箱格式是否正确。
311
+
312
+ :param email: 邮箱地址
313
+ :return: 是否符合邮箱格式
314
+ """
315
+ pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
316
+ return bool(re.match(pattern, email))
317
+
318
+ @staticmethod
319
+ def create_token(identity, claims=None, is_refresh=False, expires_delta=None) -> dict:
320
+ """
321
+ 为用户端生成令牌。
322
+
323
+ :param identity: 用户身份标识
324
+ :param claims: 其他声明信息
325
+ :param is_refresh: 是否生成刷新令牌
326
+ :param expires_delta: 过期时间,可以是 timedelta 对象或 datetime 对象,如果为 None 则使用默认配置
327
+ :return: 包含访问令牌和可选的刷新令牌的字典
328
+ """
329
+ if claims is None:
330
+ claims = {"role": "admin"}
331
+ access_token = create_access_token(identity=identity, additional_claims=claims, expires_delta=expires_delta)
332
+ if is_refresh:
333
+ return {"access_token": access_token}
334
+ refresh_token = create_refresh_token(identity=identity, additional_claims=claims, expires_delta=expires_delta)
335
+ return {"access_token": access_token, "refresh_token": refresh_token}
336
+
337
+ @staticmethod
338
+ def _get_fields(item) -> list:
339
+ """
340
+ 获取查询结果项的字段名。
341
+ :param item: 查询结果项
342
+ :return: 字段名列表
343
+ """
344
+ if hasattr(item, '__table__'):
345
+ return item.__table__.columns.keys()
346
+ elif hasattr(item, '_asdict'):
347
+ return item._asdict().keys()
348
+ elif isinstance(item, tuple):
349
+ # 如果是命名元组,则返回其._fields属性;否则,使用索引作为键
350
+ return getattr(item, '_fields', None) or range(len(item))
351
+ else:
352
+ return ['value']
353
+
354
+ @staticmethod
355
+ def query_to_dict(item) -> dict:
356
+ """
357
+ 将单个查询结果转换为字典。
358
+ :param item: 查询结果对象
359
+ :return: 字典
360
+ """
361
+ fields = Utils._get_fields(item)
362
+ if not fields:
363
+ return {}
364
+ return {field: getattr(item, field, item[field] if isinstance(item, (list, tuple)) else item) for field in
365
+ fields}
366
+
367
+ @staticmethod
368
+ def sanitize_field_name(field) -> None:
369
+ # 如果字段名以数字开头,则添加前缀'_'
370
+ return '_' + field if field[0].isdigit() else field
371
+
372
+ @staticmethod
373
+ def query_to_dict_list(data: any) -> list:
374
+ """
375
+ 将查询结果列表转换为字典列表。
376
+ :param data: 查询结果列表
377
+ :return: 字典列表
378
+ """
379
+ if not data:
380
+ return []
381
+
382
+ # 获取字段名(假设 Utils._get_fields 是提取对象/行中的字段名)
383
+ fields = Utils._get_fields(data[0])
384
+
385
+ result = []
386
+ for item in data:
387
+ item_dict = {}
388
+ for field in fields:
389
+ # 处理字段名,如果以数字开头则添加'_'
390
+ sanitized_field = Utils.sanitize_field_name(field)
391
+
392
+ # 根据item的类型获取值
393
+ if isinstance(item, (list, tuple)):
394
+ # 如果是列表或元组,则用索引访问
395
+ value = item[fields.index(sanitized_field)] if sanitized_field in fields else None
396
+ else:
397
+ # 如果是对象或其他结构,尝试 getattr
398
+ value = getattr(item, sanitized_field, None)
399
+
400
+ if isinstance(value, Decimal):
401
+ value = float(value)
402
+
403
+ item_dict[field] = value
404
+ result.append(item_dict)
405
+
406
+ return result
407
+
408
+ @staticmethod
409
+ def _register_backend_submodules(blueprint_name: str, backend_dir) -> None:
410
+ """
411
+ 自动注册 backend 目录下的所有子模块到 sys.modules
412
+ 支持 from {blueprint_name}.service import xxx 语法
413
+ """
414
+ import sys
415
+ import importlib.util
416
+ import types
417
+ from pathlib import Path
418
+
419
+ backend_path = Path(backend_dir)
420
+ if not backend_path.exists():
421
+ return
422
+
423
+ # 遍历 backend 目录下的所有子目录(service, schema, constant, common, model 等)
424
+ for subdir in backend_path.iterdir():
425
+ if not subdir.is_dir() or subdir.name.startswith('_'):
426
+ continue
427
+
428
+ # 跳过 controller,它有单独的加载逻辑
429
+ if subdir.name == 'controller':
430
+ continue
431
+
432
+ submodule_name = f"{blueprint_name}.{subdir.name}"
433
+
434
+ # 如果已经注册,跳过
435
+ if submodule_name in sys.modules:
436
+ continue
437
+
438
+ # 创建模块对象
439
+ submodule = types.ModuleType(submodule_name)
440
+ submodule.__path__ = [str(subdir)]
441
+ submodule.__file__ = str(subdir / '__init__.py')
442
+ sys.modules[submodule_name] = submodule
443
+
444
+ # 加载 __init__.py(如果存在)
445
+ init_file = subdir / '__init__.py'
446
+ if init_file.exists():
447
+ try:
448
+ spec = importlib.util.spec_from_file_location(
449
+ submodule_name, init_file,
450
+ submodule_search_locations=[str(subdir)]
451
+ )
452
+ if spec and spec.loader:
453
+ spec.loader.exec_module(submodule)
454
+ except Exception:
455
+ pass
456
+
457
+ # 注册子模块中的所有 .py 文件
458
+ for py_file in subdir.glob('*.py'):
459
+ if py_file.name.startswith('__'):
460
+ continue
461
+ file_module_name = f"{submodule_name}.{py_file.stem}"
462
+ if file_module_name in sys.modules:
463
+ continue
464
+ try:
465
+ spec = importlib.util.spec_from_file_location(file_module_name, py_file)
466
+ if spec and spec.loader:
467
+ file_module = importlib.util.module_from_spec(spec)
468
+ sys.modules[file_module_name] = file_module
469
+ spec.loader.exec_module(file_module)
470
+ except Exception:
471
+ pass
472
+
473
+ @staticmethod
474
+ def import_controller(package_name) -> None:
475
+ """
476
+ 导入指定包下的所有控制器模块
477
+ 支持 modules/*/backend/controller 目录结构
478
+ 自动注册所有兄弟子模块(service, schema 等)
479
+ """
480
+ import sys
481
+ import importlib.util
482
+ import types
483
+ from pathlib import Path
484
+
485
+ # 获取包路径
486
+ package_path = package_name.replace('.', '/')
487
+ module_name_parts = package_path.split('/')
488
+ blueprint_name = module_name_parts[0] # 如 'admin'
489
+
490
+ # 尝试多个可能的路径
491
+ project_root = Utils.get_project_root()
492
+ possible_paths = [
493
+ (Path(project_root) / package_path, None), # 直接路径
494
+ (Path(project_root) / 'modules' / blueprint_name / 'backend' / '/'.join(module_name_parts[1:]),
495
+ Path(project_root) / 'modules' / blueprint_name / 'backend'), # modules/admin/backend/controller
496
+ ]
497
+
498
+ for controller_dir, backend_dir in possible_paths:
499
+ if controller_dir.exists() and controller_dir.is_dir():
500
+ # 找到有效的 controller 目录
501
+
502
+ # 先自动注册 backend 下的所有兄弟子模块(service, schema, constant 等)
503
+ if backend_dir:
504
+ Utils._register_backend_submodules(blueprint_name, backend_dir)
505
+
506
+ # 注册 controller 模块
507
+ if package_name not in sys.modules:
508
+ controller_module = types.ModuleType(package_name)
509
+ controller_module.__path__ = [str(controller_dir)]
510
+ controller_module.__file__ = str(controller_dir / '__init__.py')
511
+ sys.modules[package_name] = controller_module
512
+
513
+ # 加载 controller/__init__.py(包含公共装饰器如 admin_required)
514
+ init_file = controller_dir / '__init__.py'
515
+ if init_file.exists():
516
+ try:
517
+ spec = importlib.util.spec_from_file_location(
518
+ package_name, init_file,
519
+ submodule_search_locations=[str(controller_dir)]
520
+ )
521
+ if spec and spec.loader:
522
+ spec.loader.exec_module(controller_module)
523
+ except Exception:
524
+ pass
525
+
526
+ # 加载所有控制器文件
527
+ for py_file in controller_dir.glob('*.py'):
528
+ if py_file.name.startswith('__'):
529
+ continue
530
+ module_file_name = py_file.stem
531
+ module_name = f"{package_name}.{module_file_name}"
532
+ try:
533
+ spec = importlib.util.spec_from_file_location(module_name, py_file)
534
+ if spec and spec.loader:
535
+ module = importlib.util.module_from_spec(spec)
536
+ sys.modules[module_name] = module
537
+ spec.loader.exec_module(module)
538
+ except Exception as e:
539
+ print(f"[WARNING] Failed to load controller {module_name}: {e}")
540
+ return
541
+
542
+ # 回退到原始方式
543
+ module_names = [name for _, name, _ in pkgutil.iter_modules([str(package_path)])]
544
+ for module_name in module_names:
545
+ full_module_path = f"{package_name}.{module_name}".replace('app.', 'fred_admin.')
546
+ importlib.import_module(full_module_path)
547
+
548
+ @staticmethod
549
+ def timestamp_to_utc(timestamp):
550
+ """
551
+ 时间戳转时区时间
552
+ """
553
+ return datetime.utcfromtimestamp(timestamp) if timestamp else None
554
+
555
+ @staticmethod
556
+ def md5_encrypt(text: str) -> str:
557
+ # 创建一个 MD5 对象
558
+ md5 = hashlib.md5()
559
+
560
+ # 更新 MD5 对象的内容(需要将字符串编码为字节)
561
+ md5.update(text.encode('utf-8'))
562
+
563
+ # 获取加密后的十六进制字符串
564
+ encrypted_text = md5.hexdigest()
565
+
566
+ return encrypted_text
567
+
568
+ @staticmethod
569
+ def timestamp_format(timestamp: int, formatted: str = '%Y-%m-%d %H:%M:%S') -> str:
570
+ """
571
+ 时间戳转时间
572
+ """
573
+ # 如果没有给时间戳则返回当前时间
574
+ if not timestamp:
575
+ return datetime.now().strftime(formatted)
576
+ return datetime.fromtimestamp(timestamp).strftime(formatted)
577
+
578
+ @staticmethod
579
+ def time_to_timestamp(time_string: str, formatted: str = '%Y-%m-%d %H:%M:%S') -> int:
580
+ """
581
+ 时间字符串转时间戳
582
+ """
583
+ try:
584
+ return int(datetime.strptime(time_string, formatted).timestamp())
585
+ except Exception as e:
586
+ return 0
587
+
588
+ @staticmethod
589
+ def datetime_to_timestamp(dt: datetime) -> int:
590
+ """
591
+ 日期转时间戳
592
+ """
593
+ try:
594
+ return int(dt.timestamp())
595
+ except Exception as e:
596
+ return 0
597
+
598
+ @staticmethod
599
+ def get_n_days_ago(n: int, formatted: str = "%Y-%m-%d %H:%M:%S"):
600
+ """
601
+ 获取N天前的时间
602
+ params formatted :如果为空返回时间戳
603
+ """
604
+ # 获取当前日期时间
605
+ n_days_ago = datetime.now() - timedelta(days=n)
606
+ if formatted == "":
607
+ # 返回时间戳
608
+ return int(n_days_ago.timestamp())
609
+ else:
610
+ # 格式化时间为 年-月-日 时:分:秒 的形式
611
+ return n_days_ago.strftime(formatted)
612
+
613
+ @staticmethod
614
+ def format_time(dt, formatted: str = '%Y-%m-%d %H:%M:%S') -> str:
615
+ """
616
+ 格式化时间对象为字符串
617
+ :param dt: datetime 对象或时间戳
618
+ :param formatted: 格式化字符串
619
+ :return: 格式化后的时间字符串
620
+ """
621
+ if dt is None:
622
+ return ''
623
+ if isinstance(dt, datetime):
624
+ return dt.strftime(formatted)
625
+ elif isinstance(dt, int):
626
+ return Utils.timestamp_format(dt, formatted)
627
+ else:
628
+ return str(dt)
629
+
630
+ @staticmethod
631
+ def format_frame_stamp(frame_stamp: str = None, formatted: str = '%Y-%m-%d %H:%M:%S') -> str:
632
+ """
633
+ 格式化帧时间戳为指定格式
634
+ 支持多种时间格式:ISO 格式、标准格式等
635
+ :param frame_stamp: 帧时间戳字符串(ISO 格式或标准格式),如:
636
+ - ISO 格式: 2025-01-01T12:00:00 或 2025-01-01T12:00:00.000
637
+ - 标准格式: 2025-01-01 12:00:00 或 2025-01-01 12:00:00.000
638
+ :param formatted: 目标格式化字符串,默认为 '%Y-%m-%d %H:%M:%S'
639
+ :return: 格式化后的时间字符串,如果无法解析则返回原始值
640
+ """
641
+ if not frame_stamp:
642
+ return None
643
+
644
+ try:
645
+ # 处理多种时间格式
646
+ normalized_stamp = frame_stamp.strip()
647
+ date = None
648
+
649
+ # 如果是 ISO 格式(包含 T)
650
+ if "T" in normalized_stamp:
651
+ try:
652
+ # 尝试解析 ISO 格式
653
+ if "." in normalized_stamp:
654
+ date = datetime.strptime(normalized_stamp, "%Y-%m-%dT%H:%M:%S.%f")
655
+ else:
656
+ date = datetime.strptime(normalized_stamp, "%Y-%m-%dT%H:%M:%S")
657
+ except ValueError:
658
+ pass
659
+ # 如果是标准格式(包含空格)
660
+ elif " " in normalized_stamp:
661
+ try:
662
+ # 尝试解析标准格式
663
+ if "." in normalized_stamp:
664
+ date = datetime.strptime(normalized_stamp, "%Y-%m-%d %H:%M:%S.%f")
665
+ else:
666
+ date = datetime.strptime(normalized_stamp, "%Y-%m-%d %H:%M:%S")
667
+ except ValueError:
668
+ pass
669
+
670
+ # 如果成功解析,格式化为指定格式
671
+ if date:
672
+ return date.strftime(formatted)
673
+
674
+ # 如果无法解析,返回原始值
675
+ return frame_stamp
676
+ except Exception:
677
+ # 如果解析失败,返回原始值
678
+ return frame_stamp
679
+
680
+ @staticmethod
681
+ def sort_json_recursively(data):
682
+ """
683
+ 递归对 JSON 对象按照 key 进行排序
684
+ :param data: 需要排序的数据(dict、list 或其他类型)
685
+ :return: 排序后的数据
686
+ """
687
+ if isinstance(data, dict):
688
+ # 对字典的 key 进行排序,然后递归处理每个值
689
+ return {key: Utils.sort_json_recursively(value) for key, value in sorted(data.items())}
690
+ elif isinstance(data, list):
691
+ # 对列表中的每个元素递归处理
692
+ return [Utils.sort_json_recursively(item) for item in data]
693
+ else:
694
+ # 对于其他类型(字符串、数字、布尔值等),直接返回
695
+ return data
696
+
697
+ @staticmethod
698
+ def upload_file(upload_folder: str, file_type: list = None, filesize: int = 10) -> dict:
699
+ """
700
+ 上传文件
701
+ :param upload_folder: 上传文件夹名称(相对于UPLOAD_FOLDER配置的路径)
702
+ :param file_type: 允许的文件类型列表,默认:['jpeg', 'jpg', 'png', 'gif']
703
+ :param filesize: 文件大小限制(MB),默认:10MB
704
+ :return: 包含file_name和msg的字典
705
+ """
706
+ data = {
707
+ "file_name": "",
708
+ "msg": ""
709
+ }
710
+
711
+ # 检查请求方法
712
+ if request.method != "POST":
713
+ data["msg"] = "上传失败:仅支持POST请求"
714
+ return data
715
+
716
+ # 获取上传路径配置(独立路径,不使用RESOURCE_PATH)
717
+ route_config = current_app.config.get('ROUTE_CONFIG', {})
718
+ upload_base_folder = route_config.get('upload', 'upload')
719
+
720
+ # 处理上传文件夹路径
721
+ if not upload_folder:
722
+ upload_folder = "default"
723
+
724
+ # 构建完整的上传路径
725
+ upload_path = os.path.join(upload_base_folder, upload_folder).replace('\\', '/')
726
+
727
+ # 设置默认文件类型
728
+ if file_type is None:
729
+ file_type = ['jpeg', 'jpg', 'png', 'gif']
730
+
731
+ # 检查文件大小
732
+ if request.content_length and request.content_length > filesize * 1024 * 1024:
733
+ data["msg"] = f"上传文件不能超过{filesize}M"
734
+ return data
735
+
736
+ # 检查文件是否存在
737
+ if 'file' not in request.files:
738
+ data["msg"] = "未找到上传文件"
739
+ return data
740
+
741
+ f = request.files['file']
742
+ if not f or f.filename == '':
743
+ data["msg"] = "文件名为空"
744
+ return data
745
+
746
+ # 检查文件扩展名
747
+ file_suffix = f.filename.rsplit('.', 1)[-1] if '.' in f.filename else ''
748
+ if not file_suffix or file_suffix.lower() not in file_type:
749
+ data["msg"] = f"文件格式错误!仅支持:{', '.join(file_type)}"
750
+ return data
751
+
752
+ # 确保上传目录存在(支持多级目录)
753
+ os.makedirs(upload_path, exist_ok=True)
754
+
755
+ # 生成唯一文件名
756
+ filename = f"{int(time())}_{randint(1000, 9000)}.{file_suffix}"
757
+
758
+ # 保存文件
759
+ try:
760
+ file_path = os.path.join(upload_path, filename)
761
+ f.save(file_path)
762
+
763
+ # 构建返回的文件路径(相对路径)
764
+ data["file_name"] = f"{upload_path}/{filename}".replace('\\', '/')
765
+
766
+ # 如果保存文件目录以../开头,保存到数据库的路径改成/开头
767
+ if data["file_name"].startswith("../"):
768
+ data["file_name"] = "/" + data["file_name"][3:]
769
+
770
+ return data
771
+ except Exception as e:
772
+ data["msg"] = f"文件保存失败:{str(e)}"
773
+ return data
774
+
775
+ @staticmethod
776
+ def get_api_urls_from_files(controller_dir: str = None) -> list:
777
+ """
778
+ 从文件扫描方式获取接口URL列表,支持扫描所有模块的controller目录
779
+ :param controller_dir: 控制器目录路径,如果为None则自动扫描所有模块
780
+ :return: 接口URL列表,每个元素包含url、method、summary等字段
781
+ """
782
+ api_urls = []
783
+
784
+ # 获取项目根目录
785
+ project_root = str(Utils.get_project_root())
786
+
787
+ # 如果指定了controller_dir,只扫描该目录
788
+ if controller_dir is not None:
789
+ if not os.path.exists(controller_dir):
790
+ return api_urls
791
+ module_dirs = [(controller_dir, 'admin')] # 默认使用admin作为蓝图名
792
+ else:
793
+ # 自动扫描所有模块目录
794
+ module_dirs = Utils._get_module_directories(project_root)
795
+
796
+ # 遍历所有模块的controller目录
797
+ for controller_dir_path, blueprint_name in module_dirs:
798
+ if not os.path.exists(controller_dir_path):
799
+ continue
800
+
801
+ # 遍历所有控制器文件
802
+ for filename in os.listdir(controller_dir_path):
803
+ if filename.endswith('.py') and not filename.startswith('__'):
804
+ file_path = os.path.join(controller_dir_path, filename)
805
+ try:
806
+ with open(file_path, 'r', encoding='utf-8') as f:
807
+ content = f.read()
808
+
809
+ # 匹配蓝图装饰器:@blueprint_name.route("path")
810
+ # 优化正则表达式,允许装饰器和类定义之间有多个空行或空白字符
811
+ # 使用 \s+ 匹配一个或多个空白字符(包括空格、制表符、换行符等)
812
+ route_class_pattern = rf'@{re.escape(blueprint_name)}\.route\(["\']([^"\']+)["\']\)\s+class\s+(\w+)'
813
+ route_class_matches = re.findall(route_class_pattern, content, re.MULTILINE)
814
+
815
+ # 调试:检查 SystemLogController 是否被扫描到
816
+ if 'SystemLogController' in filename:
817
+ # 临时调试输出(生产环境可以移除)
818
+ pass
819
+
820
+ for route_path, class_name in route_class_matches:
821
+ # 规范化路径:确保以/开头
822
+ normalized_route_path = route_path.strip()
823
+ if not normalized_route_path.startswith('/'):
824
+ normalized_route_path = '/' + normalized_route_path
825
+
826
+ # 获取模块的url_prefix(从__init__.py中读取)
827
+ module_name = os.path.basename(os.path.dirname(controller_dir_path))
828
+ url_prefix = Utils._get_module_url_prefix(project_root, module_name)
829
+
830
+ # 组合完整路径:url_prefix + route_path
831
+ full_path = url_prefix + normalized_route_path
832
+
833
+ # 找到这个类的完整定义
834
+ # 使用更精确的类匹配模式,确保能匹配到类的完整内容
835
+ class_pattern = rf'class\s+{re.escape(class_name)}\s*\([^)]*\)\s*:(.*?)(?=class\s+\w+|@\w+\.route|\Z)'
836
+ class_match = re.search(class_pattern, content, re.MULTILINE | re.DOTALL)
837
+
838
+ if class_match:
839
+ class_content = class_match.group(1)
840
+ # 在类内容中查找所有方法
841
+ # 优化方法匹配,允许方法参数中有空格
842
+ method_pattern = r'def\s+(\w+)\s*\([^)]*self'
843
+ methods = re.findall(method_pattern, class_content)
844
+
845
+ # 为每个方法创建接口
846
+ for method in methods:
847
+ # 检查方法名是否为HTTP方法(不区分大小写)
848
+ method_upper = method.upper()
849
+ if method_upper in ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']:
850
+ # 尝试从代码注释中提取接口描述
851
+ summary = Utils.extract_api_summary_from_code(class_content, method)
852
+
853
+ api_urls.append({
854
+ 'url': full_path,
855
+ 'method': method_upper,
856
+ 'summary': summary,
857
+ 'description': summary,
858
+ 'operation_id': f"{class_name}_{method}",
859
+ 'tags': [class_name],
860
+ 'label': f"{method_upper} {full_path} - {summary}"
861
+ })
862
+
863
+ except Exception as e:
864
+ # 静默跳过有问题的文件,避免影响整体扫描
865
+ # 如果某个文件解析失败,只记录但不中断整个扫描过程
866
+ continue
867
+
868
+ # 去重(同一路径和方法的接口)
869
+ unique_api_urls = []
870
+ seen = set()
871
+ for api in api_urls:
872
+ key = (api['url'], api['method'])
873
+ if key not in seen:
874
+ seen.add(key)
875
+ unique_api_urls.append(api)
876
+
877
+ # 按URL和方法排序
878
+ unique_api_urls.sort(key=lambda x: (x['url'], x['method']))
879
+
880
+ return unique_api_urls
881
+
882
+ @staticmethod
883
+ def _get_module_directories(project_root: str) -> list:
884
+ """
885
+ 获取所有模块的controller目录路径和蓝图名称
886
+ 目录结构:modules/{module}/backend/controller/
887
+ :param project_root: 项目根目录
888
+ :return: [(controller_dir_path, blueprint_name), ...]
889
+ """
890
+ module_dirs = []
891
+
892
+ # 扫描 modules 目录下的所有模块
893
+ modules_base = os.path.join(project_root, 'modules')
894
+ if not os.path.exists(modules_base) or not os.path.isdir(modules_base):
895
+ return module_dirs
896
+
897
+ for module_name in os.listdir(modules_base):
898
+ module_path = os.path.join(modules_base, module_name)
899
+ if not os.path.isdir(module_path):
900
+ continue
901
+
902
+ # 目录结构:modules/{module}/backend/controller/
903
+ controller_path = os.path.join(module_path, 'backend', 'controller')
904
+ if os.path.exists(controller_path) and os.path.isdir(controller_path):
905
+ # 从 backend/__init__.py 中获取蓝图变量名
906
+ init_file = os.path.join(module_path, 'backend', '__init__.py')
907
+ blueprint_name = Utils._get_blueprint_name_from_init(init_file, module_name)
908
+ module_dirs.append((controller_path, blueprint_name))
909
+
910
+ return module_dirs
911
+
912
+ @staticmethod
913
+ def _get_blueprint_name_from_init(init_file_path: str, default_name: str) -> str:
914
+ """
915
+ 从模块的__init__.py文件中获取蓝图变量名
916
+ :param init_file_path: __init__.py文件路径
917
+ :param default_name: 默认蓝图名称
918
+ :return: 蓝图变量名
919
+ """
920
+ if not os.path.exists(init_file_path):
921
+ return default_name
922
+
923
+ try:
924
+ with open(init_file_path, 'r', encoding='utf-8') as f:
925
+ content = f.read()
926
+
927
+ # 匹配 Blueprint('name', ...) 或 variable = Blueprint(...)
928
+ # 优先匹配 variable = Blueprint(...) 格式
929
+ blueprint_pattern = r'(\w+)\s*=\s*Blueprint\(["\'](\w+)["\']'
930
+ match = re.search(blueprint_pattern, content)
931
+ if match:
932
+ return match.group(1) # 返回变量名
933
+
934
+ # 如果没有找到,返回默认名称
935
+ return default_name
936
+ except Exception:
937
+ return default_name
938
+
939
+ @staticmethod
940
+ def _get_module_url_prefix(project_root: str, module_name: str) -> str:
941
+ """
942
+ 从模块的__init__.py文件中获取url_prefix
943
+ 目录结构:modules/{module}/backend/__init__.py
944
+ :param project_root: 项目根目录
945
+ :param module_name: 模块名称
946
+ :return: url_prefix
947
+ """
948
+ init_file = os.path.join(project_root, 'modules', module_name, 'backend', '__init__.py')
949
+
950
+ if not os.path.exists(init_file):
951
+ return f'/{module_name}'
952
+
953
+ try:
954
+ with open(init_file, 'r', encoding='utf-8') as f:
955
+ content = f.read()
956
+
957
+ # 匹配 url_prefix="/xxx"
958
+ url_prefix_pattern = r'url_prefix\s*=\s*["\']([^"\']+)["\']'
959
+ match = re.search(url_prefix_pattern, content)
960
+ if match:
961
+ prefix = match.group(1)
962
+ # 确保以/开头
963
+ if not prefix.startswith('/'):
964
+ prefix = '/' + prefix
965
+ return prefix
966
+
967
+ # 如果没有找到,返回默认前缀
968
+ return f'/{module_name}'
969
+ except Exception:
970
+ return f'/{module_name}'
971
+
972
+ @staticmethod
973
+ def extract_api_summary_from_code(class_content: str, method: str) -> str:
974
+ """
975
+ 从代码注释中提取接口描述,支持多种注释格式
976
+ :param class_content: 类的内容字符串
977
+ :param method: 方法名
978
+ :return: 接口描述
979
+ """
980
+ # 查找方法的定义和注释
981
+ method_pattern = rf'def\s+{method}\s*\([^)]*\):.*?(?=def|\Z)'
982
+ method_match = re.search(method_pattern, class_content, re.MULTILINE | re.DOTALL)
983
+
984
+ if not method_match:
985
+ return f"{method.title()}方法"
986
+
987
+ method_code = method_match.group(0)
988
+
989
+ # 尝试提取多种格式的注释
990
+ patterns = [
991
+ # Flask-Smorest 格式: """接口描述"""
992
+ r'"""(.*?)"""',
993
+ # 普通注释格式: # 接口描述
994
+ r'#\s*(.+?)(?:\n|$)',
995
+ # 多行注释格式: """\n接口描述\n"""
996
+ r'"""\s*\n\s*(.+?)\s*\n\s*"""',
997
+ # 单行注释: # 接口描述
998
+ r'#\s*([^\n]+)',
999
+ ]
1000
+
1001
+ for pattern in patterns:
1002
+ matches = re.findall(pattern, method_code, re.MULTILINE | re.DOTALL)
1003
+ for match in matches:
1004
+ # 清理注释内容
1005
+ summary = match.strip()
1006
+ # 移除多余的空白字符
1007
+ summary = re.sub(r'\s+', ' ', summary)
1008
+ # 如果找到有效的描述,返回
1009
+ if summary and len(summary) > 2:
1010
+ return summary
1011
+
1012
+ # 如果没有找到注释,直接显示方法名
1013
+ return method.title()
1014
+
1015
+ @staticmethod
1016
+ def get_api_summary_by_url_and_method(api_url: str, method: str, api_list: list = None) -> str:
1017
+ """
1018
+ 根据接口URL和方法获取接口说明
1019
+ :param api_url: 接口URL(可能包含模块前缀,如/admin、/terminal等)
1020
+ :param method: 请求方法
1021
+ :param api_list: 接口列表,如果为None则自动获取
1022
+ :return: 接口说明
1023
+ """
1024
+ if not api_url or not method:
1025
+ return ''
1026
+
1027
+ if api_list is None:
1028
+ api_list = Utils.get_api_urls_from_files()
1029
+
1030
+ # 规范化API URL:保留完整路径(包含模块前缀)
1031
+ normalized_url = api_url.strip()
1032
+ # 确保以/开头
1033
+ if not normalized_url.startswith('/'):
1034
+ normalized_url = '/' + normalized_url
1035
+ # 移除末尾的斜杠(如果有)用于比较
1036
+ normalized_url_compare = normalized_url.rstrip('/')
1037
+
1038
+ # 规范化method(转换为大写)
1039
+ normalized_method = method.strip().upper() if method else ''
1040
+
1041
+ # 查找匹配的接口
1042
+ for api in api_list:
1043
+ api_url_in_list = api.get('url', '').strip()
1044
+ api_method = str(api.get('method', '')).strip().upper()
1045
+
1046
+ # 匹配方法(必须完全匹配)
1047
+ if api_method != normalized_method:
1048
+ continue
1049
+
1050
+ # 规范化列表中的URL用于比较
1051
+ if not api_url_in_list.startswith('/'):
1052
+ api_url_in_list = '/' + api_url_in_list
1053
+ api_url_normalized = api_url_in_list.rstrip('/')
1054
+
1055
+ # 精确匹配路径(忽略末尾斜杠)
1056
+ if api_url_normalized == normalized_url_compare:
1057
+ summary = api.get('summary', '')
1058
+ return summary if summary else ''
1059
+
1060
+ return ''