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.
- fred_admin/__init__.py +91 -0
- fred_admin/common/AliyunSms.py +66 -0
- fred_admin/common/Blueprints.py +99 -0
- fred_admin/common/Email.py +23 -0
- fred_admin/common/Extensions.py +319 -0
- fred_admin/common/HandleExcetion.py +204 -0
- fred_admin/common/ImageVerification.py +117 -0
- fred_admin/common/IoTDBClient.py +698 -0
- fred_admin/common/PageSchema.py +65 -0
- fred_admin/common/Response.py +251 -0
- fred_admin/common/Route.py +390 -0
- fred_admin/common/RuntimeHook.py +111 -0
- fred_admin/common/SchedulerLog.py +277 -0
- fred_admin/common/Sqlacodegen.py +170 -0
- fred_admin/common/Swagger.py +77 -0
- fred_admin/common/SystemLog.py +80 -0
- fred_admin/common/Utils.py +1060 -0
- fred_admin/common/WechatLogin.py +97 -0
- fred_admin/common/__init__.py +3 -0
- fred_admin/config/Config.py +104 -0
- fred_admin/config/Logger.py +34 -0
- fred_admin/config/__init__.py +3 -0
- fred_admin/create_module.py +787 -0
- fred_admin/demo/app.py +6 -0
- fred_admin/demo/common/__init__.py +1 -0
- fred_admin/demo/common/config/Config.py +82 -0
- fred_admin/demo/common/config/README.md +8 -0
- fred_admin/demo/common/config/__init__.py +1 -0
- fred_admin/demo/common/config/__pycache__/Config.cpython-312.pyc +0 -0
- fred_admin/demo/common/config/__pycache__/__init__.cpython-312.pyc +0 -0
- fred_admin/demo/common/config/jwt_secret_key +1 -0
- fred_admin/demo/common/config/session_secret_key +1 -0
- fred_admin/demo/common/model/__pycache__/__init__.cpython-312.pyc +0 -0
- fred_admin/demo/common/model/__pycache__/model.cpython-312.pyc +0 -0
- fred_admin/demo/common/model/model.py +274 -0
- fred_admin/demo/demo_.gitignore +42 -0
- fred_admin/demo/modules/__init__.py +1 -0
- fred_admin/demo/modules/admin/README.md +38 -0
- fred_admin/demo/modules/admin/__init__.py +3 -0
- fred_admin/demo/modules/admin/backend/__init__.py +7 -0
- fred_admin/demo/modules/admin/backend/constant/RedisKey.py +10 -0
- fred_admin/demo/modules/admin/backend/controller/AdminAuthController.py +39 -0
- fred_admin/demo/modules/admin/backend/controller/AdminController.py +226 -0
- fred_admin/demo/modules/admin/backend/controller/ApiAuthController.py +93 -0
- fred_admin/demo/modules/admin/backend/controller/ButtonManageController.py +90 -0
- fred_admin/demo/modules/admin/backend/controller/FileController.py +28 -0
- fred_admin/demo/modules/admin/backend/controller/OrganizationController.py +218 -0
- fred_admin/demo/modules/admin/backend/controller/RoleController.py +143 -0
- fred_admin/demo/modules/admin/backend/controller/SystemController.py +112 -0
- fred_admin/demo/modules/admin/backend/controller/SystemLogController.py +63 -0
- fred_admin/demo/modules/admin/backend/controller/TableController.py +175 -0
- fred_admin/demo/modules/admin/backend/controller/__init__.py +55 -0
- fred_admin/demo/modules/admin/backend/schema/AdminSchema.py +356 -0
- fred_admin/demo/modules/admin/backend/schema/ApiAuthSchema.py +72 -0
- fred_admin/demo/modules/admin/backend/schema/AuthMenuSchema.py +79 -0
- fred_admin/demo/modules/admin/backend/schema/ButtonManageSchema.py +98 -0
- fred_admin/demo/modules/admin/backend/schema/OrganizationSchema.py +134 -0
- fred_admin/demo/modules/admin/backend/schema/RoleSchema.py +94 -0
- fred_admin/demo/modules/admin/backend/schema/SystemConfigSchema.py +115 -0
- fred_admin/demo/modules/admin/backend/schema/TableSchema.py +153 -0
- fred_admin/demo/modules/admin/backend/schema/UploadImg.py +35 -0
- fred_admin/demo/modules/admin/backend/schema/__init__.py +0 -0
- fred_admin/demo/modules/admin/backend/schema/__pycache__/AdminSchema.cpython-312.pyc +0 -0
- fred_admin/demo/modules/admin/backend/schema/__pycache__/ApiAuthSchema.cpython-312.pyc +0 -0
- fred_admin/demo/modules/admin/backend/schema/__pycache__/AuthMenuSchema.cpython-312.pyc +0 -0
- fred_admin/demo/modules/admin/backend/schema/__pycache__/ButtonManageSchema.cpython-312.pyc +0 -0
- fred_admin/demo/modules/admin/backend/schema/__pycache__/OrganizationSchema.cpython-312.pyc +0 -0
- fred_admin/demo/modules/admin/backend/schema/__pycache__/RoleSchema.cpython-312.pyc +0 -0
- fred_admin/demo/modules/admin/backend/schema/__pycache__/SystemConfigSchema.cpython-312.pyc +0 -0
- fred_admin/demo/modules/admin/backend/schema/__pycache__/TableSchema.cpython-312.pyc +0 -0
- fred_admin/demo/modules/admin/backend/schema/__pycache__/UploadImg.cpython-312.pyc +0 -0
- fred_admin/demo/modules/admin/backend/schema/__pycache__/__init__.cpython-312.pyc +0 -0
- fred_admin/demo/modules/admin/backend/service/AdminService.py +377 -0
- fred_admin/demo/modules/admin/backend/service/ApiAuthService.py +237 -0
- fred_admin/demo/modules/admin/backend/service/AuthButtonsService.py +155 -0
- fred_admin/demo/modules/admin/backend/service/AuthMenuService.py +265 -0
- fred_admin/demo/modules/admin/backend/service/ButtonManageService.py +386 -0
- fred_admin/demo/modules/admin/backend/service/ButtonManageService.py.broken +113 -0
- fred_admin/demo/modules/admin/backend/service/FileService.py +33 -0
- fred_admin/demo/modules/admin/backend/service/OrganizationService.py +465 -0
- fred_admin/demo/modules/admin/backend/service/RoleService.py +445 -0
- fred_admin/demo/modules/admin/backend/service/SystemConfigService.py +89 -0
- fred_admin/demo/modules/admin/backend/service/SystemLogService.py +142 -0
- fred_admin/demo/modules/admin/backend/service/TableService.py +978 -0
- fred_admin/demo/modules/admin/backend/service/__init__.py +1 -0
- fred_admin/demo/modules/admin/backend/sql/app.db +0 -0
- fred_admin/demo/modules/admin/backend/sql/fred.sql +3780 -0
- fred_admin/demo/modules/admin/frontend/LICENSE +21 -0
- fred_admin/demo/modules/admin/frontend/build/getEnv.ts +46 -0
- fred_admin/demo/modules/admin/frontend/build/plugins.ts +118 -0
- fred_admin/demo/modules/admin/frontend/build/proxy.ts +30 -0
- fred_admin/demo/modules/admin/frontend/commitlint.config.cjs +162 -0
- fred_admin/demo/modules/admin/frontend/demo_editorconfig +15 -0
- fred_admin/demo/modules/admin/frontend/demo_env +15 -0
- fred_admin/demo/modules/admin/frontend/demo_env.development +21 -0
- fred_admin/demo/modules/admin/frontend/demo_env.production +25 -0
- fred_admin/demo/modules/admin/frontend/demo_eslintignore +15 -0
- fred_admin/demo/modules/admin/frontend/demo_eslintrc.cjs +61 -0
- fred_admin/demo/modules/admin/frontend/demo_gitignore +27 -0
- fred_admin/demo/modules/admin/frontend/demo_prettierignore +9 -0
- fred_admin/demo/modules/admin/frontend/demo_prettierrc.cjs +41 -0
- fred_admin/demo/modules/admin/frontend/demo_stylelintignore +4 -0
- fred_admin/demo/modules/admin/frontend/demo_stylelintrc.cjs +40 -0
- fred_admin/demo/modules/admin/frontend/index.html +105 -0
- fred_admin/demo/modules/admin/frontend/lint-staged.config.cjs +8 -0
- fred_admin/demo/modules/admin/frontend/package.json +129 -0
- fred_admin/demo/modules/admin/frontend/pnpm-lock.yaml +11150 -0
- fred_admin/demo/modules/admin/frontend/postcss.config.cjs +5 -0
- fred_admin/demo/modules/admin/frontend/public/logo.png +0 -0
- fred_admin/demo/modules/admin/frontend/public/vue.svg +1 -0
- fred_admin/demo/modules/admin/frontend/src/App.vue +44 -0
- fred_admin/demo/modules/admin/frontend/src/api/config/servicePort.ts +2 -0
- fred_admin/demo/modules/admin/frontend/src/api/helper/axiosCancel.ts +57 -0
- fred_admin/demo/modules/admin/frontend/src/api/helper/checkStatus.ts +43 -0
- fred_admin/demo/modules/admin/frontend/src/api/index.ts +167 -0
- fred_admin/demo/modules/admin/frontend/src/api/interface/index.ts +179 -0
- fred_admin/demo/modules/admin/frontend/src/api/model/adminModel.ts +135 -0
- fred_admin/demo/modules/admin/frontend/src/api/model/apiAuthModel.ts +72 -0
- fred_admin/demo/modules/admin/frontend/src/api/model/index.ts +24 -0
- fred_admin/demo/modules/admin/frontend/src/api/model/loginModel.ts +79 -0
- fred_admin/demo/modules/admin/frontend/src/api/model/roleModel.ts +68 -0
- fred_admin/demo/modules/admin/frontend/src/api/model/storeModel.ts +80 -0
- fred_admin/demo/modules/admin/frontend/src/api/model/systemModel.ts +162 -0
- fred_admin/demo/modules/admin/frontend/src/api/model/uploadModel.ts +153 -0
- fred_admin/demo/modules/admin/frontend/src/api/modules/admin.ts +76 -0
- fred_admin/demo/modules/admin/frontend/src/api/modules/apiAuth.ts +53 -0
- fred_admin/demo/modules/admin/frontend/src/api/modules/buttonManage.ts +119 -0
- fred_admin/demo/modules/admin/frontend/src/api/modules/dataBase.ts +60 -0
- fred_admin/demo/modules/admin/frontend/src/api/modules/login.ts +54 -0
- fred_admin/demo/modules/admin/frontend/src/api/modules/organization.ts +123 -0
- fred_admin/demo/modules/admin/frontend/src/api/modules/role.ts +61 -0
- fred_admin/demo/modules/admin/frontend/src/api/modules/store.ts +303 -0
- fred_admin/demo/modules/admin/frontend/src/api/modules/system.ts +125 -0
- fred_admin/demo/modules/admin/frontend/src/api/modules/upload.ts +132 -0
- fred_admin/demo/modules/admin/frontend/src/api/modules/user.ts +63 -0
- fred_admin/demo/modules/admin/frontend/src/assets/fonts/DIN.otf +0 -0
- fred_admin/demo/modules/admin/frontend/src/assets/fonts/MetroDF.ttf +0 -0
- fred_admin/demo/modules/admin/frontend/src/assets/fonts/YouSheBiaoTiHei.ttf +0 -0
- fred_admin/demo/modules/admin/frontend/src/assets/fonts/font.scss +14 -0
- fred_admin/demo/modules/admin/frontend/src/assets/iconfont/iconfont.scss +51 -0
- fred_admin/demo/modules/admin/frontend/src/assets/iconfont/iconfont.ttf +0 -0
- fred_admin/demo/modules/admin/frontend/src/assets/icons/xianxingdaoyu.svg +1 -0
- fred_admin/demo/modules/admin/frontend/src/assets/icons/xianxingdiqiu.svg +1 -0
- fred_admin/demo/modules/admin/frontend/src/assets/icons/xianxingditu.svg +1 -0
- fred_admin/demo/modules/admin/frontend/src/assets/icons/xianxingfanchuan.svg +1 -0
- fred_admin/demo/modules/admin/frontend/src/assets/icons/xianxingfeiji.svg +1 -0
- fred_admin/demo/modules/admin/frontend/src/assets/icons/xianxinglvhangriji.svg +1 -0
- fred_admin/demo/modules/admin/frontend/src/assets/icons/xianxingtianqiyubao.svg +1 -0
- fred_admin/demo/modules/admin/frontend/src/assets/icons/xianxingxiangjipaizhao.svg +1 -0
- fred_admin/demo/modules/admin/frontend/src/assets/icons/xianxingxiarilengyin.svg +1 -0
- fred_admin/demo/modules/admin/frontend/src/assets/icons/xianxingyoulun.svg +1 -0
- fred_admin/demo/modules/admin/frontend/src/assets/icons/xianxingzijiayou.svg +1 -0
- fred_admin/demo/modules/admin/frontend/src/assets/images/403.png +0 -0
- fred_admin/demo/modules/admin/frontend/src/assets/images/404.png +0 -0
- fred_admin/demo/modules/admin/frontend/src/assets/images/500.png +0 -0
- fred_admin/demo/modules/admin/frontend/src/assets/images/avatar.gif +0 -0
- fred_admin/demo/modules/admin/frontend/src/assets/images/login_bg.svg +33 -0
- fred_admin/demo/modules/admin/frontend/src/assets/images/login_left.png +0 -0
- fred_admin/demo/modules/admin/frontend/src/assets/images/login_left1.png +0 -0
- fred_admin/demo/modules/admin/frontend/src/assets/images/login_left2.png +0 -0
- fred_admin/demo/modules/admin/frontend/src/assets/images/login_left3.png +0 -0
- fred_admin/demo/modules/admin/frontend/src/assets/images/login_left4.png +0 -0
- fred_admin/demo/modules/admin/frontend/src/assets/images/login_left5.png +0 -0
- fred_admin/demo/modules/admin/frontend/src/assets/images/logo.svg +1 -0
- fred_admin/demo/modules/admin/frontend/src/assets/images/msg01.png +0 -0
- fred_admin/demo/modules/admin/frontend/src/assets/images/msg02.png +0 -0
- fred_admin/demo/modules/admin/frontend/src/assets/images/msg03.png +0 -0
- fred_admin/demo/modules/admin/frontend/src/assets/images/msg04.png +0 -0
- fred_admin/demo/modules/admin/frontend/src/assets/images/msg05.png +0 -0
- fred_admin/demo/modules/admin/frontend/src/assets/images/notData.png +0 -0
- fred_admin/demo/modules/admin/frontend/src/assets/images/welcome.png +0 -0
- fred_admin/demo/modules/admin/frontend/src/assets/json/authButtonList.json +8 -0
- fred_admin/demo/modules/admin/frontend/src/assets/json/authMenuList.json +1075 -0
- fred_admin/demo/modules/admin/frontend/src/assets/mock/Easy-Mock-API.zip +0 -0
- fred_admin/demo/modules/admin/frontend/src/components/ECharts/config/index.ts +72 -0
- fred_admin/demo/modules/admin/frontend/src/components/ECharts/index.vue +104 -0
- fred_admin/demo/modules/admin/frontend/src/components/ErrorMessage/403.vue +19 -0
- fred_admin/demo/modules/admin/frontend/src/components/ErrorMessage/404.vue +19 -0
- fred_admin/demo/modules/admin/frontend/src/components/ErrorMessage/500.vue +19 -0
- fred_admin/demo/modules/admin/frontend/src/components/ErrorMessage/index.scss +32 -0
- fred_admin/demo/modules/admin/frontend/src/components/Grid/components/GridItem.vue +68 -0
- fred_admin/demo/modules/admin/frontend/src/components/Grid/index.vue +167 -0
- fred_admin/demo/modules/admin/frontend/src/components/Grid/interface/index.ts +6 -0
- fred_admin/demo/modules/admin/frontend/src/components/ImageCaptcha/index.vue +71 -0
- fred_admin/demo/modules/admin/frontend/src/components/ImportExcel/index.scss +3 -0
- fred_admin/demo/modules/admin/frontend/src/components/ImportExcel/index.vue +151 -0
- fred_admin/demo/modules/admin/frontend/src/components/JsonViewer/index.vue +249 -0
- fred_admin/demo/modules/admin/frontend/src/components/Loading/fullScreen.ts +45 -0
- fred_admin/demo/modules/admin/frontend/src/components/Loading/index.scss +67 -0
- fred_admin/demo/modules/admin/frontend/src/components/Loading/index.vue +13 -0
- fred_admin/demo/modules/admin/frontend/src/components/ProTable/components/ColSetting.vue +45 -0
- fred_admin/demo/modules/admin/frontend/src/components/ProTable/components/Pagination.vue +101 -0
- fred_admin/demo/modules/admin/frontend/src/components/ProTable/components/TableColumn.vue +58 -0
- fred_admin/demo/modules/admin/frontend/src/components/ProTable/index.vue +348 -0
- fred_admin/demo/modules/admin/frontend/src/components/ProTable/interface/index.ts +87 -0
- fred_admin/demo/modules/admin/frontend/src/components/SearchForm/components/SearchFormItem.vue +96 -0
- fred_admin/demo/modules/admin/frontend/src/components/SearchForm/index.vue +98 -0
- fred_admin/demo/modules/admin/frontend/src/components/SelectFilter/index.scss +63 -0
- fred_admin/demo/modules/admin/frontend/src/components/SelectFilter/index.vue +110 -0
- fred_admin/demo/modules/admin/frontend/src/components/SelectIcon/index.scss +39 -0
- fred_admin/demo/modules/admin/frontend/src/components/SelectIcon/index.vue +90 -0
- fred_admin/demo/modules/admin/frontend/src/components/StoreFilter/index.vue +356 -0
- fred_admin/demo/modules/admin/frontend/src/components/SvgIcon/index.vue +22 -0
- fred_admin/demo/modules/admin/frontend/src/components/SwitchDark/index.vue +12 -0
- fred_admin/demo/modules/admin/frontend/src/components/TreeFilter/index.scss +44 -0
- fred_admin/demo/modules/admin/frontend/src/components/TreeFilter/index.vue +165 -0
- fred_admin/demo/modules/admin/frontend/src/components/Upload/Img.vue +304 -0
- fred_admin/demo/modules/admin/frontend/src/components/Upload/Imgs.vue +316 -0
- fred_admin/demo/modules/admin/frontend/src/components/WangEditor/index.scss +28 -0
- fred_admin/demo/modules/admin/frontend/src/components/WangEditor/index.vue +154 -0
- fred_admin/demo/modules/admin/frontend/src/config/amapMap.ts +11 -0
- fred_admin/demo/modules/admin/frontend/src/config/index.ts +16 -0
- fred_admin/demo/modules/admin/frontend/src/config/nprogress.ts +12 -0
- fred_admin/demo/modules/admin/frontend/src/directives/index.ts +28 -0
- fred_admin/demo/modules/admin/frontend/src/directives/modules/auth.ts +26 -0
- fred_admin/demo/modules/admin/frontend/src/directives/modules/copy.ts +37 -0
- fred_admin/demo/modules/admin/frontend/src/directives/modules/debounce.ts +31 -0
- fred_admin/demo/modules/admin/frontend/src/directives/modules/draggable.ts +49 -0
- fred_admin/demo/modules/admin/frontend/src/directives/modules/longpress.ts +49 -0
- fred_admin/demo/modules/admin/frontend/src/directives/modules/throttle.ts +41 -0
- fred_admin/demo/modules/admin/frontend/src/directives/modules/waterMarker.ts +36 -0
- fred_admin/demo/modules/admin/frontend/src/enums/httpEnum.ts +35 -0
- fred_admin/demo/modules/admin/frontend/src/hooks/interface/index.ts +32 -0
- fred_admin/demo/modules/admin/frontend/src/hooks/useAuthButtons.ts +22 -0
- fred_admin/demo/modules/admin/frontend/src/hooks/useDownload.ts +42 -0
- fred_admin/demo/modules/admin/frontend/src/hooks/useHandleData.ts +41 -0
- fred_admin/demo/modules/admin/frontend/src/hooks/useI18n.ts +51 -0
- fred_admin/demo/modules/admin/frontend/src/hooks/useOnline.ts +31 -0
- fred_admin/demo/modules/admin/frontend/src/hooks/useSelection.ts +34 -0
- fred_admin/demo/modules/admin/frontend/src/hooks/useTable.ts +150 -0
- fred_admin/demo/modules/admin/frontend/src/hooks/useTheme.ts +111 -0
- fred_admin/demo/modules/admin/frontend/src/hooks/useTime.ts +38 -0
- fred_admin/demo/modules/admin/frontend/src/languages/index.ts +32 -0
- fred_admin/demo/modules/admin/frontend/src/languages/modules/en.ts +978 -0
- fred_admin/demo/modules/admin/frontend/src/languages/modules/zh.ts +996 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/LayoutClassic/index.scss +60 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/LayoutClassic/index.vue +62 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/LayoutColumns/index.scss +95 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/LayoutColumns/index.vue +103 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/LayoutTransverse/index.scss +60 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/LayoutTransverse/index.vue +61 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/LayoutVertical/index.scss +48 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/LayoutVertical/index.vue +56 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/Footer/index.scss +11 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/Footer/index.vue +9 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/ToolBarLeft.vue +23 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/ToolBarRight.vue +51 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/components/AssemblySize.vue +37 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/components/Avatar.vue +79 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/components/Breadcrumb.vue +122 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/components/CollapseIcon.vue +21 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/components/Fullscreen.vue +25 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/components/InfoDialog.vue +22 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/components/Language.vue +38 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/components/Message.vue +109 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/components/PasswordDialog.vue +81 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/components/SearchMenu.vue +183 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/Header/components/ThemeSetting.vue +12 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/Main/components/Maximize.vue +39 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/Main/index.scss +10 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/Main/index.vue +88 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/Menu/SubMenu.vue +85 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/Tabs/components/MoreButton.vue +85 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/Tabs/index.scss +69 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/Tabs/index.vue +115 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/ThemeDrawer/index.scss +137 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/components/ThemeDrawer/index.vue +191 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/index.vue +42 -0
- fred_admin/demo/modules/admin/frontend/src/layouts/indexAsync.vue +46 -0
- fred_admin/demo/modules/admin/frontend/src/main.ts +45 -0
- fred_admin/demo/modules/admin/frontend/src/routers/index.ts +106 -0
- fred_admin/demo/modules/admin/frontend/src/routers/modules/dynamicRouter.ts +99 -0
- fred_admin/demo/modules/admin/frontend/src/routers/modules/staticRouter.ts +63 -0
- fred_admin/demo/modules/admin/frontend/src/stores/helper/persist.ts +19 -0
- fred_admin/demo/modules/admin/frontend/src/stores/index.ts +8 -0
- fred_admin/demo/modules/admin/frontend/src/stores/interface/index.ts +63 -0
- fred_admin/demo/modules/admin/frontend/src/stores/modules/auth.ts +50 -0
- fred_admin/demo/modules/admin/frontend/src/stores/modules/global.ts +55 -0
- fred_admin/demo/modules/admin/frontend/src/stores/modules/keepAlive.ts +25 -0
- fred_admin/demo/modules/admin/frontend/src/stores/modules/tabs.ts +78 -0
- fred_admin/demo/modules/admin/frontend/src/stores/modules/user.ts +23 -0
- fred_admin/demo/modules/admin/frontend/src/styles/common.scss +129 -0
- fred_admin/demo/modules/admin/frontend/src/styles/element-dark.scss +28 -0
- fred_admin/demo/modules/admin/frontend/src/styles/element.scss +264 -0
- fred_admin/demo/modules/admin/frontend/src/styles/modules/annotation.scss +746 -0
- fred_admin/demo/modules/admin/frontend/src/styles/modules/assembly.scss +714 -0
- fred_admin/demo/modules/admin/frontend/src/styles/modules/device-manager.scss +659 -0
- fred_admin/demo/modules/admin/frontend/src/styles/modules/directives.scss +381 -0
- fred_admin/demo/modules/admin/frontend/src/styles/reset.scss +143 -0
- fred_admin/demo/modules/admin/frontend/src/styles/table-optimization.scss +242 -0
- fred_admin/demo/modules/admin/frontend/src/styles/theme/aside.ts +16 -0
- fred_admin/demo/modules/admin/frontend/src/styles/theme/header.ts +25 -0
- fred_admin/demo/modules/admin/frontend/src/styles/theme/menu.ts +31 -0
- fred_admin/demo/modules/admin/frontend/src/styles/var.scss +2 -0
- fred_admin/demo/modules/admin/frontend/src/typings/global.d.ts +74 -0
- fred_admin/demo/modules/admin/frontend/src/typings/utils.d.ts +17 -0
- fred_admin/demo/modules/admin/frontend/src/typings/window.d.ts +8 -0
- fred_admin/demo/modules/admin/frontend/src/utils/color.ts +59 -0
- fred_admin/demo/modules/admin/frontend/src/utils/dict.ts +17 -0
- fred_admin/demo/modules/admin/frontend/src/utils/eleValidate.ts +14 -0
- fred_admin/demo/modules/admin/frontend/src/utils/errorHandler.ts +28 -0
- fred_admin/demo/modules/admin/frontend/src/utils/index.ts +327 -0
- fred_admin/demo/modules/admin/frontend/src/utils/is/index.ts +125 -0
- fred_admin/demo/modules/admin/frontend/src/utils/mittBus.ts +5 -0
- fred_admin/demo/modules/admin/frontend/src/utils/svg.ts +13 -0
- fred_admin/demo/modules/admin/frontend/src/views/dataBase/fieldType/components/FieldTypeDrawer.vue +89 -0
- fred_admin/demo/modules/admin/frontend/src/views/dataBase/fieldType/index.vue +99 -0
- fred_admin/demo/modules/admin/frontend/src/views/dataBase/tableManage/components/DataBaseDrawer.vue +254 -0
- fred_admin/demo/modules/admin/frontend/src/views/dataBase/tableManage/components/IndexDrawer.vue +226 -0
- fred_admin/demo/modules/admin/frontend/src/views/dataBase/tableManage/components/TableDataDrawer.vue +136 -0
- fred_admin/demo/modules/admin/frontend/src/views/dataBase/tableManage/index.vue +234 -0
- fred_admin/demo/modules/admin/frontend/src/views/echarts/columnChart/index.scss +0 -0
- fred_admin/demo/modules/admin/frontend/src/views/echarts/columnChart/index.vue +139 -0
- fred_admin/demo/modules/admin/frontend/src/views/echarts/lineChart/index.scss +0 -0
- fred_admin/demo/modules/admin/frontend/src/views/echarts/lineChart/index.vue +123 -0
- fred_admin/demo/modules/admin/frontend/src/views/echarts/nestedChart/index.scss +0 -0
- fred_admin/demo/modules/admin/frontend/src/views/echarts/nestedChart/index.vue +97 -0
- fred_admin/demo/modules/admin/frontend/src/views/echarts/pieChart/index.scss +0 -0
- fred_admin/demo/modules/admin/frontend/src/views/echarts/pieChart/index.vue +68 -0
- fred_admin/demo/modules/admin/frontend/src/views/echarts/radarChart/index.scss +0 -0
- fred_admin/demo/modules/admin/frontend/src/views/echarts/radarChart/index.vue +56 -0
- fred_admin/demo/modules/admin/frontend/src/views/echarts/waterChart/index.scss +0 -0
- fred_admin/demo/modules/admin/frontend/src/views/echarts/waterChart/index.vue +298 -0
- fred_admin/demo/modules/admin/frontend/src/views/home/index.scss +12 -0
- fred_admin/demo/modules/admin/frontend/src/views/home/index.vue +11 -0
- fred_admin/demo/modules/admin/frontend/src/views/login/components/LoginForm.vue +147 -0
- fred_admin/demo/modules/admin/frontend/src/views/login/index.scss +83 -0
- fred_admin/demo/modules/admin/frontend/src/views/login/index.vue +27 -0
- fred_admin/demo/modules/admin/frontend/src/views/proTable/complexProTable/index.vue +176 -0
- fred_admin/demo/modules/admin/frontend/src/views/proTable/components/UserDrawer.vue +152 -0
- fred_admin/demo/modules/admin/frontend/src/views/proTable/document/index.vue +13 -0
- fred_admin/demo/modules/admin/frontend/src/views/proTable/treeProTable/index.vue +129 -0
- fred_admin/demo/modules/admin/frontend/src/views/proTable/useProTable/detail.vue +12 -0
- fred_admin/demo/modules/admin/frontend/src/views/proTable/useProTable/index.vue +258 -0
- fred_admin/demo/modules/admin/frontend/src/views/proTable/useSelectFilter/index.vue +184 -0
- fred_admin/demo/modules/admin/frontend/src/views/proTable/useTreeFilter/detail.vue +12 -0
- fred_admin/demo/modules/admin/frontend/src/views/proTable/useTreeFilter/index.vue +147 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/accountManage/index.vue +244 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/apiAuth/components/ApiDrawer.vue +114 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/apiAuth/components/TokenDrawer.vue +160 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/apiAuth/index.vue +354 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/buttonManage/components/ButtonDrawer.vue +460 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/buttonManage/index.vue +426 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/configManage/components/ConfigDrawer.vue +106 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/configManage/index.vue +116 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/departmentManage/index.vue +7 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/dictManage/index.vue +7 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/menuManage/components/MenuDrawer.vue +262 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/menuManage/index.vue +310 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/organizationManage/components/CompanyDrawer.vue +72 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/organizationManage/components/DepartmentDrawer.vue +120 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/organizationManage/components/TeamDrawer.vue +162 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/organizationManage/components/TeamMemberDrawer.vue +243 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/organizationManage/index.vue +414 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/roleManage/components/RoleButtonDrawer.vue +587 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/roleManage/components/RoleDrawer.vue +77 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/roleManage/components/RoleMenuDrawer.vue +236 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/roleManage/components/RoleUserDrawer.vue +125 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/roleManage/index.vue +143 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/systemLog/components/SystemLogDrawer.vue +207 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/systemLog/index.vue +219 -0
- fred_admin/demo/modules/admin/frontend/src/views/system/timingTask/index.vue +7 -0
- fred_admin/demo/modules/admin/frontend/src/vite-env.d.ts +7 -0
- fred_admin/demo/modules/admin/frontend/tsconfig.json +42 -0
- fred_admin/demo/modules/admin/frontend/vite.config.ts +91 -0
- 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
- fred_admin/demo/requirements.txt +1 -0
- fred_admin/fonts/NotoSansSC-VariableFont_wght.ttf +0 -0
- fred_admin/install_hook.py +1633 -0
- fred_admin/setup.py +18 -0
- fred_admin-1.0.0.dist-info/METADATA +97 -0
- fred_admin-1.0.0.dist-info/RECORD +376 -0
- fred_admin-1.0.0.dist-info/WHEEL +5 -0
- fred_admin-1.0.0.dist-info/entry_points.txt +3 -0
- fred_admin-1.0.0.dist-info/licenses/LICENSE +21 -0
- 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 ''
|