From ebc096498812218b0500858b63b93aa4cd7d9346 Mon Sep 17 00:00:00 2001 From: tangwei Date: Tue, 31 Mar 2026 11:32:47 +0800 Subject: [PATCH] 11 --- backend/platform-common/pom.xml | 42 ++ .../common/request/RequestParams.java | 48 ++ .../common/response/ResponseResult.java | 60 ++ .../yfd/platform/common/utils/CallBack.java | 45 ++ .../platform/common/utils/CodeGenerator.java | 75 ++ .../common/utils/EncryptConfigUtil.java | 25 + .../platform/common/utils/EncryptUtils.java | 100 +++ .../platform/common/utils/ExecutionJob.java | 102 +++ .../yfd/platform/common/utils/FileUtil.java | 395 ++++++++++ .../platform/common/utils/MpGenerator.java | 80 ++ .../common/utils/ObjectConverterUtil.java | 152 ++++ .../common/utils/PropertiesUtils.java | 29 + .../platform/common/utils/QuartzManage.java | 187 +++++ .../platform/common/utils/QuartzRunnable.java | 59 ++ .../platform/common/utils/RequestHolder.java | 34 + .../yfd/platform/common/utils/RsaUtils.java | 190 +++++ .../platform/common/utils/SecurityUtils.java | 85 +++ .../common/utils/SpringContextHolder.java | 145 ++++ .../platform/common/utils/StringUtils.java | 306 ++++++++ backend/platform-system/pom.xml | 49 ++ .../platform/system/PlatformApplication.java | 46 ++ .../platform/system/ServletInitializer.java | 14 + .../system/annotation/AnonymousAccess.java | 30 + .../yfd/platform/system/annotation/Log.java | 20 + .../annotation/rest/AnonymousGetMapping.java | 86 +++ .../yfd/platform/system/aspect/LogAspect.java | 93 +++ .../component/ServerSendEventServer.java | 147 ++++ .../system/component/WebSocketServer.java | 106 +++ .../system/config/AppInitProperties.java | 17 + .../system/config/DataInitializer.java | 129 ++++ .../system/config/FileProperties.java | 59 ++ .../system/config/FileSpaceProperties.java | 17 + .../system/config/GlobalExceptionHandler.java | 25 + .../yfd/platform/system/config/JobRunner.java | 59 ++ .../config/JwtAuthenticationTokenFilter.java | 83 +++ .../platform/system/config/MessageConfig.java | 50 ++ .../system/config/MybitsPlusConfig.java | 35 + .../system/config/ProdApiPrefixFilter.java | 44 ++ .../platform/system/config/QuartzConfig.java | 77 ++ .../system/config/ResponseResult.java | 57 ++ .../system/config/SecurityConfig.java | 90 +++ .../platform/system/config/SwaggerConfig.java | 49 ++ .../yfd/platform/system/config/WebConfig.java | 61 ++ .../system/config/WebSocketConfig.java | 16 + .../system/config/bean/LoginCode.java | 61 ++ .../system/config/bean/LoginCodeEnum.java | 43 ++ .../system/config/bean/LoginProperties.java | 110 +++ .../config/thread/AsyncTaskExecutePool.java | 70 ++ .../config/thread/AsyncTaskProperties.java | 39 + .../config/thread/TheadFactoryName.java | 62 ++ .../config/thread/ThreadPoolExecutorUtil.java | 44 ++ .../platform/system/constant/Constant.java | 42 ++ .../system/datasource/DataSource.java | 17 + .../system/datasource/DataSourceAspect.java | 55 ++ .../system/datasource/DynamicDataSource.java | 40 + .../datasource/DynamicDataSourceConfig.java | 51 ++ .../exception/AccessDeniedHandExcetion.java | 27 + .../exception/AuthenticationException.java | 29 + .../exception/BadConfigurationException.java | 98 +++ .../system/exception/BadRequestException.java | 41 ++ .../exception/ChildrenExistException.java | 20 + .../exception/EntityExistException.java | 34 + .../exception/EntityNotFoundException.java | 34 + .../controller/DataSourceController.java | 42 ++ .../system/controller/LoginController.java | 238 ++++++ .../system/controller/MessageController.java | 150 ++++ .../controller/QuartzJobController.java | 183 +++++ .../system/controller/SSEController.java | 59 ++ .../controller/SysConfigController.java | 68 ++ .../controller/SysDictionaryController.java | 142 ++++ .../SysDictionaryItemsController.java | 201 +++++ .../system/controller/SysLogController.java | 74 ++ .../system/controller/SysMenuController.java | 310 ++++++++ .../controller/SysOrganizationController.java | 213 ++++++ .../system/controller/SysRoleController.java | 318 ++++++++ .../system/controller/UserController.java | 188 +++++ .../system/system/domain/Dictionary.java | 78 ++ .../system/system/domain/LoginUser.java | 76 ++ .../system/system/domain/Message.java | 117 +++ .../system/system/domain/QuartzJob.java | 118 +++ .../system/system/domain/SysConfig.java | 78 ++ .../system/system/domain/SysDictionary.java | 73 ++ .../system/domain/SysDictionaryItems.java | 79 ++ .../platform/system/system/domain/SysLog.java | 94 +++ .../system/system/domain/SysMenu.java | 114 +++ .../system/system/domain/SysOrganization.java | 93 +++ .../system/system/domain/SysRole.java | 99 +++ .../system/system/domain/SysUser.java | 120 +++ .../system/system/mapper/MessageMapper.java | 16 + .../system/system/mapper/QuartzJobMapper.java | 16 + .../system/system/mapper/SysConfigMapper.java | 17 + .../mapper/SysDictionaryItemsMapper.java | 17 + .../system/mapper/SysDictionaryMapper.java | 22 + .../system/system/mapper/SysLogMapper.java | 16 + .../system/system/mapper/SysMenuMapper.java | 59 ++ .../system/mapper/SysOrganizationMapper.java | 33 + .../system/system/mapper/SysRoleMapper.java | 112 +++ .../system/system/mapper/SysUserMapper.java | 96 +++ .../system/service/IMessageService.java | 16 + .../system/service/IQuartzJobService.java | 43 ++ .../system/service/ISysConfigService.java | 22 + .../service/ISysDictionaryItemsService.java | 47 ++ .../system/service/ISysDictionaryService.java | 45 ++ .../system/system/service/ISysLogService.java | 52 ++ .../system/service/ISysMenuService.java | 101 +++ .../service/ISysOrganizationService.java | 60 ++ .../system/service/ISysRoleService.java | 68 ++ .../system/system/service/IUserService.java | 143 ++++ .../service/impl/MessageServiceImpl.java | 20 + .../service/impl/QuartzJobServiceImpl.java | 114 +++ .../service/impl/SysConfigServiceImpl.java | 34 + .../impl/SysDictionaryItemsServiceImpl.java | 123 ++++ .../impl/SysDictionaryServiceImpl.java | 114 +++ .../service/impl/SysLogServiceImpl.java | 219 ++++++ .../service/impl/SysMenuServiceImpl.java | 695 ++++++++++++++++++ .../impl/SysOrganizationServiceImpl.java | 363 +++++++++ .../service/impl/SysRoleServiceImpl.java | 174 +++++ .../service/impl/UserDetailsServiceImpl.java | 52 ++ .../system/service/impl/UserServiceImpl.java | 579 +++++++++++++++ .../yfd/platform/system/task/TaskMessage.java | 57 ++ .../yfd/platform/system/utils/CallBack.java | 43 ++ .../platform/system/utils/CodeGenerator.java | 75 ++ .../system/utils/EncryptConfigUtil.java | 28 + .../platform/system/utils/EncryptUtils.java | 100 +++ .../platform/system/utils/ExecutionJob.java | 103 +++ .../yfd/platform/system/utils/FileUtil.java | 398 ++++++++++ .../platform/system/utils/MpGenerator.java | 79 ++ .../system/utils/ObjectConverterUtil.java | 149 ++++ .../system/utils/PropertiesUtils.java | 29 + .../platform/system/utils/QuartzManage.java | 187 +++++ .../platform/system/utils/QuartzRunnable.java | 58 ++ .../platform/system/utils/RequestHolder.java | 34 + .../yfd/platform/system/utils/RsaUtils.java | 190 +++++ .../platform/system/utils/SecurityUtils.java | 84 +++ .../system/utils/SpringContextHolder.java | 145 ++++ .../platform/system/utils/StringUtils.java | 305 ++++++++ .../src/main/resources/application-dev.yml | 94 +++ .../src/main/resources/application-devtw.yml | 94 +++ .../main/resources/application-framework.yml | 30 + .../src/main/resources/application-server.yml | 52 ++ .../src/main/resources/application.yml | 34 + .../src/main/resources/banner.txt | 8 + .../src/main/resources/ip2region/ip2region.db | 0 .../main/resources/log4jdbc.log4j2.properties | 4 + .../src/main/resources/logback-spring.xml | 38 + .../src/main/resources/logback.xml | 45 ++ .../mapper/system/DictionaryMapper.xml | 5 + .../resources/mapper/system/MessageMapper.xml | 5 + .../resources/mapper/system/Model3dMapper.xml | 5 + .../mapper/system/QuartzJobMapper.xml | 5 + .../mapper/system/SysConfigMapper.xml | 5 + .../system/SysDictionaryItemsMapper.xml | 5 + .../mapper/system/SysDictionaryMapper.xml | 15 + .../resources/mapper/system/SysLogMapper.xml | 5 + .../resources/mapper/system/SysMenuMapper.xml | 103 +++ .../mapper/system/SysMessageMapper.xml | 5 + .../mapper/system/SysOrganizationMapper.xml | 14 + .../mapper/system/SysQuartzJobMapper.xml | 5 + .../resources/mapper/system/SysRoleMapper.xml | 150 ++++ .../resources/mapper/system/SysUserMapper.xml | 116 +++ .../src/main/resources/quartz.properties | 21 + .../resources/static/assets/401-099a3a32.js | 1 + .../resources/static/assets/401-485a4475.js | 1 + .../resources/static/assets/401-a61ddb94.gif | Bin 0 -> 164227 bytes .../resources/static/assets/401-fffd1e4b.css | 1 + .../resources/static/assets/404-51ac6f86.css | 1 + .../resources/static/assets/404-538aa4d7.png | Bin 0 -> 98071 bytes .../resources/static/assets/404-6dcbdda2.js | 1 + .../resources/static/assets/404-ae343fa7.js | 1 + .../static/assets/404_cloud-98e7ac66.png | Bin 0 -> 4766 bytes .../static/assets/BarChart-a4765ae3.js | 1 + .../static/assets/BarChart-efd5cbe1.js | 1 + .../static/assets/FunnelChart-79f3d5f7.js | 1 + .../static/assets/FunnelChart-8e41d306.js | 1 + .../static/assets/PieChart-bffd7bcc.js | 1 + .../static/assets/PieChart-f0d9d351.js | 1 + .../static/assets/RadarChart-94b1112a.js | 1 + .../static/assets/RadarChart-e43ec971.js | 1 + .../static/assets/editor-501cf061.css | 1 + .../static/assets/editor-b13c93a6.js | 186 +++++ .../static/assets/editor-ec3491e5.js | 186 +++++ .../static/assets/index-013c92bf.css | 1 + .../resources/static/assets/index-01e2cfa9.js | 1 + .../resources/static/assets/index-04bb232d.js | 1 + .../resources/static/assets/index-0985e541.js | 1 + .../resources/static/assets/index-09ad406e.js | 1 + .../resources/static/assets/index-175944b3.js | 18 + .../static/assets/index-1a396a09.css | 1 + .../resources/static/assets/index-1e7d81fe.js | 1 + .../resources/static/assets/index-2814df08.js | 1 + .../resources/static/assets/index-2a2e686f.js | 1 + .../static/assets/index-2b88764d.css | 1 + .../resources/static/assets/index-2be13ce1.js | 1 + .../static/assets/index-362b32fa.css | 1 + .../resources/static/assets/index-373114d3.js | 1 + .../static/assets/index-3e4e0c0c.css | 1 + .../static/assets/index-3ea31a03.css | 1 + .../resources/static/assets/index-486d1d98.js | 1 + .../static/assets/index-4e36f11e.css | 1 + .../resources/static/assets/index-5282e30f.js | 1 + .../resources/static/assets/index-5682e1da.js | 1 + .../static/assets/index-5a5afaa7.css | 1 + .../resources/static/assets/index-5c62e6c4.js | 83 +++ .../resources/static/assets/index-63bba755.js | 1 + .../resources/static/assets/index-6cf33161.js | 1 + .../static/assets/index-6ee17396.css | 1 + .../resources/static/assets/index-70f67d2a.js | 18 + .../resources/static/assets/index-86feac6e.js | 1 + .../resources/static/assets/index-9057b190.js | 1 + .../static/assets/index-968257e1.css | 1 + .../resources/static/assets/index-98c36269.js | 1 + .../static/assets/index-9c04fca4.css | 1 + .../resources/static/assets/index-a59640f3.js | 1 + .../static/assets/index-a7bce641.css | 1 + .../resources/static/assets/index-b25f0d08.js | 96 +++ .../static/assets/index-b5bae886.css | 1 + .../static/assets/index-b5bb9b55.css | 1 + .../resources/static/assets/index-b98a9d85.js | 1 + .../resources/static/assets/index-bca4e108.js | 1 + .../resources/static/assets/index-bcf8ef0f.js | 1 + .../resources/static/assets/index-c03bc2fe.js | 1 + .../resources/static/assets/index-c2e52e75.js | 1 + .../resources/static/assets/index-ca91e236.js | 1 + .../resources/static/assets/index-cbe38f69.js | 1 + .../static/assets/index-cdc0bc49.css | 1 + .../resources/static/assets/index-ce8e85c4.js | 1 + .../static/assets/index-dd0c8cf0.css | 1 + .../resources/static/assets/index-de9a584a.js | 1 + .../resources/static/assets/index-e051e64c.js | 1 + .../resources/static/assets/index-e5abaec0.js | 1 + .../resources/static/assets/index-eb03182a.js | 1 + .../resources/static/assets/index-f43bf226.js | 1 + .../resources/static/assets/index-f52936ec.js | 1 + .../resources/static/assets/index-f88fe59a.js | 1 + .../resources/static/assets/index-fa740669.js | 1 + .../resources/static/assets/index-fc9ca505.js | 1 + .../static/assets/index1-5c7d9d99.js | 1 + .../static/assets/index1-ac4606af.js | 1 + .../static/assets/index2-26043b81.js | 1 + .../static/assets/index2-46aebe19.js | 1 + .../static/assets/indicator-5b15d0d1.png | Bin 0 -> 341643 bytes .../static/assets/lbcz_sc-0ed76926.js | 1 + .../static/assets/lbcz_td-b5984317.js | 1 + .../static/assets/lbcz_xg-6b0694a6.js | 1 + .../resources/static/assets/logo-03d6d6da.png | Bin 0 -> 6849 bytes ...ue_type_script_setup_true_lang-42348387.js | 1 + ...ue_type_script_setup_true_lang-7a09a11a.js | 1 + .../static/assets/personalCenter-a026f3ae.js | 1 + .../static/assets/personalCenter-a1e83f26.js | 1 + .../static/assets/personalCenter-ad68cb91.css | 1 + .../static/assets/resize-24879ea2.js | 60 ++ .../static/assets/resize-9f0962b6.js | 60 ++ .../static/assets/rsaEncrypt-96cab0ea.js | 29 + .../static/assets/sortable.esm-616533ae.js | 6 + .../static/assets/tagsView-319ee48a.js | 1 + .../static/assets/tagsView-6df0ea3e.js | 1 + .../static/assets/top_tx-3cab94c6.png | Bin 0 -> 28367 bytes .../resources/static/assets/u287-9a3328bc.gif | Bin 0 -> 246181 bytes .../static/assets/uploader-0562c8e7.js | 1 + .../static/assets/uploader-343a71b2.js | 1 + .../static/assets/uploader-4183de44.css | 1 + .../src/main/resources/static/favicon.ico | Bin 0 -> 4286 bytes .../icon/0312cfde741a47ad9dfd2b6379c24229.png | Bin 0 -> 630 bytes .../icon/1376916838a345799b96ac1eacc8608f.png | Bin 0 -> 520 bytes .../icon/1449347c56414681a321dc3c84302b00.png | Bin 0 -> 630 bytes .../icon/56b7117b688a40699246aa378119c005.png | Bin 0 -> 520 bytes .../icon/78e0a2de1f8e4354ad4bc46101ff7b1d.png | Bin 0 -> 570 bytes .../icon/88d05ff3ffa74bcd9f80584c9edf300d.png | Bin 0 -> 630 bytes .../icon/91a883092f2a40ee84b135aea9640fd1.png | Bin 0 -> 431 bytes .../icon/aff16e96f9164e5196942b3738c55c10.png | Bin 0 -> 630 bytes .../icon/beaea0bdfd514f61b451d400e93f81b4.png | Bin 0 -> 498 bytes .../icon/c6cdc92296c24d168c8c08aa2009d7ca.png | Bin 0 -> 527 bytes .../icon/cc9abc741e7d444e9d8736056d76cb49.png | Bin 0 -> 414 bytes .../icon/cf3e2b0ace7a42a7b627cdd4929b21d1.png | Bin 0 -> 640 bytes .../src/main/resources/static/index.html | 17 + backend/qgc-eng-server/pom.xml | 43 ++ .../eng/controller/EngController.java | 47 ++ .../yfd/platform/eng/service/IEngService.java | 35 + .../eng/service/impl/EngServiceImpl.java | 43 ++ backend/qgc-env-server/pom.xml | 43 ++ .../env/controller/EnvController.java | 47 ++ .../yfd/platform/env/service/IEnvService.java | 35 + .../env/service/impl/EnvServiceImpl.java | 43 ++ 283 files changed, 15453 insertions(+) create mode 100644 backend/platform-common/pom.xml create mode 100644 backend/platform-common/src/main/java/com/yfd/platform/common/request/RequestParams.java create mode 100644 backend/platform-common/src/main/java/com/yfd/platform/common/response/ResponseResult.java create mode 100644 backend/platform-common/src/main/java/com/yfd/platform/common/utils/CallBack.java create mode 100644 backend/platform-common/src/main/java/com/yfd/platform/common/utils/CodeGenerator.java create mode 100644 backend/platform-common/src/main/java/com/yfd/platform/common/utils/EncryptConfigUtil.java create mode 100644 backend/platform-common/src/main/java/com/yfd/platform/common/utils/EncryptUtils.java create mode 100644 backend/platform-common/src/main/java/com/yfd/platform/common/utils/ExecutionJob.java create mode 100644 backend/platform-common/src/main/java/com/yfd/platform/common/utils/FileUtil.java create mode 100644 backend/platform-common/src/main/java/com/yfd/platform/common/utils/MpGenerator.java create mode 100644 backend/platform-common/src/main/java/com/yfd/platform/common/utils/ObjectConverterUtil.java create mode 100644 backend/platform-common/src/main/java/com/yfd/platform/common/utils/PropertiesUtils.java create mode 100644 backend/platform-common/src/main/java/com/yfd/platform/common/utils/QuartzManage.java create mode 100644 backend/platform-common/src/main/java/com/yfd/platform/common/utils/QuartzRunnable.java create mode 100644 backend/platform-common/src/main/java/com/yfd/platform/common/utils/RequestHolder.java create mode 100644 backend/platform-common/src/main/java/com/yfd/platform/common/utils/RsaUtils.java create mode 100644 backend/platform-common/src/main/java/com/yfd/platform/common/utils/SecurityUtils.java create mode 100644 backend/platform-common/src/main/java/com/yfd/platform/common/utils/SpringContextHolder.java create mode 100644 backend/platform-common/src/main/java/com/yfd/platform/common/utils/StringUtils.java create mode 100644 backend/platform-system/pom.xml create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/PlatformApplication.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/ServletInitializer.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/annotation/AnonymousAccess.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/annotation/Log.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/annotation/rest/AnonymousGetMapping.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/aspect/LogAspect.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/component/ServerSendEventServer.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/component/WebSocketServer.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/config/AppInitProperties.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/config/DataInitializer.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/config/FileProperties.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/config/FileSpaceProperties.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/config/GlobalExceptionHandler.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/config/JobRunner.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/config/JwtAuthenticationTokenFilter.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/config/MessageConfig.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/config/MybitsPlusConfig.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/config/ProdApiPrefixFilter.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/config/QuartzConfig.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/config/ResponseResult.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/config/SecurityConfig.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/config/SwaggerConfig.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/config/WebConfig.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/config/WebSocketConfig.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/config/bean/LoginCode.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/config/bean/LoginCodeEnum.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/config/bean/LoginProperties.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/config/thread/AsyncTaskExecutePool.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/config/thread/AsyncTaskProperties.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/config/thread/TheadFactoryName.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/config/thread/ThreadPoolExecutorUtil.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/constant/Constant.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/datasource/DataSource.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/datasource/DataSourceAspect.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/datasource/DynamicDataSource.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/datasource/DynamicDataSourceConfig.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/exception/AccessDeniedHandExcetion.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/exception/AuthenticationException.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/exception/BadConfigurationException.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/exception/BadRequestException.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/exception/ChildrenExistException.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/exception/EntityExistException.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/exception/EntityNotFoundException.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/DataSourceController.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/LoginController.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/MessageController.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/QuartzJobController.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SSEController.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysConfigController.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysDictionaryController.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysDictionaryItemsController.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysLogController.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysMenuController.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysOrganizationController.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysRoleController.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/UserController.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/Dictionary.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/LoginUser.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/Message.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/QuartzJob.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysConfig.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysDictionary.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysDictionaryItems.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysLog.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysMenu.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysOrganization.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysRole.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysUser.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/MessageMapper.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/QuartzJobMapper.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysConfigMapper.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysDictionaryItemsMapper.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysDictionaryMapper.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysLogMapper.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysMenuMapper.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysOrganizationMapper.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysRoleMapper.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysUserMapper.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/service/IMessageService.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/service/IQuartzJobService.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysConfigService.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysDictionaryItemsService.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysDictionaryService.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysLogService.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysMenuService.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysOrganizationService.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysRoleService.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/service/IUserService.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/MessageServiceImpl.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/QuartzJobServiceImpl.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysConfigServiceImpl.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysDictionaryItemsServiceImpl.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysDictionaryServiceImpl.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysLogServiceImpl.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysMenuServiceImpl.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysOrganizationServiceImpl.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysRoleServiceImpl.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/UserDetailsServiceImpl.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/UserServiceImpl.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/task/TaskMessage.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/utils/CallBack.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/utils/CodeGenerator.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/utils/EncryptConfigUtil.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/utils/EncryptUtils.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/utils/ExecutionJob.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/utils/FileUtil.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/utils/MpGenerator.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/utils/ObjectConverterUtil.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/utils/PropertiesUtils.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/utils/QuartzManage.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/utils/QuartzRunnable.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/utils/RequestHolder.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/utils/RsaUtils.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/utils/SecurityUtils.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/utils/SpringContextHolder.java create mode 100644 backend/platform-system/src/main/java/com/yfd/platform/system/utils/StringUtils.java create mode 100644 backend/platform-system/src/main/resources/application-dev.yml create mode 100644 backend/platform-system/src/main/resources/application-devtw.yml create mode 100644 backend/platform-system/src/main/resources/application-framework.yml create mode 100644 backend/platform-system/src/main/resources/application-server.yml create mode 100644 backend/platform-system/src/main/resources/application.yml create mode 100644 backend/platform-system/src/main/resources/banner.txt create mode 100644 backend/platform-system/src/main/resources/ip2region/ip2region.db create mode 100644 backend/platform-system/src/main/resources/log4jdbc.log4j2.properties create mode 100644 backend/platform-system/src/main/resources/logback-spring.xml create mode 100644 backend/platform-system/src/main/resources/logback.xml create mode 100644 backend/platform-system/src/main/resources/mapper/system/DictionaryMapper.xml create mode 100644 backend/platform-system/src/main/resources/mapper/system/MessageMapper.xml create mode 100644 backend/platform-system/src/main/resources/mapper/system/Model3dMapper.xml create mode 100644 backend/platform-system/src/main/resources/mapper/system/QuartzJobMapper.xml create mode 100644 backend/platform-system/src/main/resources/mapper/system/SysConfigMapper.xml create mode 100644 backend/platform-system/src/main/resources/mapper/system/SysDictionaryItemsMapper.xml create mode 100644 backend/platform-system/src/main/resources/mapper/system/SysDictionaryMapper.xml create mode 100644 backend/platform-system/src/main/resources/mapper/system/SysLogMapper.xml create mode 100644 backend/platform-system/src/main/resources/mapper/system/SysMenuMapper.xml create mode 100644 backend/platform-system/src/main/resources/mapper/system/SysMessageMapper.xml create mode 100644 backend/platform-system/src/main/resources/mapper/system/SysOrganizationMapper.xml create mode 100644 backend/platform-system/src/main/resources/mapper/system/SysQuartzJobMapper.xml create mode 100644 backend/platform-system/src/main/resources/mapper/system/SysRoleMapper.xml create mode 100644 backend/platform-system/src/main/resources/mapper/system/SysUserMapper.xml create mode 100644 backend/platform-system/src/main/resources/quartz.properties create mode 100644 backend/platform-system/src/main/resources/static/assets/401-099a3a32.js create mode 100644 backend/platform-system/src/main/resources/static/assets/401-485a4475.js create mode 100644 backend/platform-system/src/main/resources/static/assets/401-a61ddb94.gif create mode 100644 backend/platform-system/src/main/resources/static/assets/401-fffd1e4b.css create mode 100644 backend/platform-system/src/main/resources/static/assets/404-51ac6f86.css create mode 100644 backend/platform-system/src/main/resources/static/assets/404-538aa4d7.png create mode 100644 backend/platform-system/src/main/resources/static/assets/404-6dcbdda2.js create mode 100644 backend/platform-system/src/main/resources/static/assets/404-ae343fa7.js create mode 100644 backend/platform-system/src/main/resources/static/assets/404_cloud-98e7ac66.png create mode 100644 backend/platform-system/src/main/resources/static/assets/BarChart-a4765ae3.js create mode 100644 backend/platform-system/src/main/resources/static/assets/BarChart-efd5cbe1.js create mode 100644 backend/platform-system/src/main/resources/static/assets/FunnelChart-79f3d5f7.js create mode 100644 backend/platform-system/src/main/resources/static/assets/FunnelChart-8e41d306.js create mode 100644 backend/platform-system/src/main/resources/static/assets/PieChart-bffd7bcc.js create mode 100644 backend/platform-system/src/main/resources/static/assets/PieChart-f0d9d351.js create mode 100644 backend/platform-system/src/main/resources/static/assets/RadarChart-94b1112a.js create mode 100644 backend/platform-system/src/main/resources/static/assets/RadarChart-e43ec971.js create mode 100644 backend/platform-system/src/main/resources/static/assets/editor-501cf061.css create mode 100644 backend/platform-system/src/main/resources/static/assets/editor-b13c93a6.js create mode 100644 backend/platform-system/src/main/resources/static/assets/editor-ec3491e5.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-013c92bf.css create mode 100644 backend/platform-system/src/main/resources/static/assets/index-01e2cfa9.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-04bb232d.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-0985e541.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-09ad406e.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-175944b3.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-1a396a09.css create mode 100644 backend/platform-system/src/main/resources/static/assets/index-1e7d81fe.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-2814df08.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-2a2e686f.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-2b88764d.css create mode 100644 backend/platform-system/src/main/resources/static/assets/index-2be13ce1.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-362b32fa.css create mode 100644 backend/platform-system/src/main/resources/static/assets/index-373114d3.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-3e4e0c0c.css create mode 100644 backend/platform-system/src/main/resources/static/assets/index-3ea31a03.css create mode 100644 backend/platform-system/src/main/resources/static/assets/index-486d1d98.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-4e36f11e.css create mode 100644 backend/platform-system/src/main/resources/static/assets/index-5282e30f.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-5682e1da.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-5a5afaa7.css create mode 100644 backend/platform-system/src/main/resources/static/assets/index-5c62e6c4.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-63bba755.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-6cf33161.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-6ee17396.css create mode 100644 backend/platform-system/src/main/resources/static/assets/index-70f67d2a.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-86feac6e.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-9057b190.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-968257e1.css create mode 100644 backend/platform-system/src/main/resources/static/assets/index-98c36269.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-9c04fca4.css create mode 100644 backend/platform-system/src/main/resources/static/assets/index-a59640f3.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-a7bce641.css create mode 100644 backend/platform-system/src/main/resources/static/assets/index-b25f0d08.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-b5bae886.css create mode 100644 backend/platform-system/src/main/resources/static/assets/index-b5bb9b55.css create mode 100644 backend/platform-system/src/main/resources/static/assets/index-b98a9d85.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-bca4e108.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-bcf8ef0f.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-c03bc2fe.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-c2e52e75.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-ca91e236.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-cbe38f69.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-cdc0bc49.css create mode 100644 backend/platform-system/src/main/resources/static/assets/index-ce8e85c4.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-dd0c8cf0.css create mode 100644 backend/platform-system/src/main/resources/static/assets/index-de9a584a.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-e051e64c.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-e5abaec0.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-eb03182a.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-f43bf226.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-f52936ec.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-f88fe59a.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-fa740669.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index-fc9ca505.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index1-5c7d9d99.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index1-ac4606af.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index2-26043b81.js create mode 100644 backend/platform-system/src/main/resources/static/assets/index2-46aebe19.js create mode 100644 backend/platform-system/src/main/resources/static/assets/indicator-5b15d0d1.png create mode 100644 backend/platform-system/src/main/resources/static/assets/lbcz_sc-0ed76926.js create mode 100644 backend/platform-system/src/main/resources/static/assets/lbcz_td-b5984317.js create mode 100644 backend/platform-system/src/main/resources/static/assets/lbcz_xg-6b0694a6.js create mode 100644 backend/platform-system/src/main/resources/static/assets/logo-03d6d6da.png create mode 100644 backend/platform-system/src/main/resources/static/assets/page.vue_vue_type_script_setup_true_lang-42348387.js create mode 100644 backend/platform-system/src/main/resources/static/assets/page.vue_vue_type_script_setup_true_lang-7a09a11a.js create mode 100644 backend/platform-system/src/main/resources/static/assets/personalCenter-a026f3ae.js create mode 100644 backend/platform-system/src/main/resources/static/assets/personalCenter-a1e83f26.js create mode 100644 backend/platform-system/src/main/resources/static/assets/personalCenter-ad68cb91.css create mode 100644 backend/platform-system/src/main/resources/static/assets/resize-24879ea2.js create mode 100644 backend/platform-system/src/main/resources/static/assets/resize-9f0962b6.js create mode 100644 backend/platform-system/src/main/resources/static/assets/rsaEncrypt-96cab0ea.js create mode 100644 backend/platform-system/src/main/resources/static/assets/sortable.esm-616533ae.js create mode 100644 backend/platform-system/src/main/resources/static/assets/tagsView-319ee48a.js create mode 100644 backend/platform-system/src/main/resources/static/assets/tagsView-6df0ea3e.js create mode 100644 backend/platform-system/src/main/resources/static/assets/top_tx-3cab94c6.png create mode 100644 backend/platform-system/src/main/resources/static/assets/u287-9a3328bc.gif create mode 100644 backend/platform-system/src/main/resources/static/assets/uploader-0562c8e7.js create mode 100644 backend/platform-system/src/main/resources/static/assets/uploader-343a71b2.js create mode 100644 backend/platform-system/src/main/resources/static/assets/uploader-4183de44.css create mode 100644 backend/platform-system/src/main/resources/static/favicon.ico create mode 100644 backend/platform-system/src/main/resources/static/icon/0312cfde741a47ad9dfd2b6379c24229.png create mode 100644 backend/platform-system/src/main/resources/static/icon/1376916838a345799b96ac1eacc8608f.png create mode 100644 backend/platform-system/src/main/resources/static/icon/1449347c56414681a321dc3c84302b00.png create mode 100644 backend/platform-system/src/main/resources/static/icon/56b7117b688a40699246aa378119c005.png create mode 100644 backend/platform-system/src/main/resources/static/icon/78e0a2de1f8e4354ad4bc46101ff7b1d.png create mode 100644 backend/platform-system/src/main/resources/static/icon/88d05ff3ffa74bcd9f80584c9edf300d.png create mode 100644 backend/platform-system/src/main/resources/static/icon/91a883092f2a40ee84b135aea9640fd1.png create mode 100644 backend/platform-system/src/main/resources/static/icon/aff16e96f9164e5196942b3738c55c10.png create mode 100644 backend/platform-system/src/main/resources/static/icon/beaea0bdfd514f61b451d400e93f81b4.png create mode 100644 backend/platform-system/src/main/resources/static/icon/c6cdc92296c24d168c8c08aa2009d7ca.png create mode 100644 backend/platform-system/src/main/resources/static/icon/cc9abc741e7d444e9d8736056d76cb49.png create mode 100644 backend/platform-system/src/main/resources/static/icon/cf3e2b0ace7a42a7b627cdd4929b21d1.png create mode 100644 backend/platform-system/src/main/resources/static/index.html create mode 100644 backend/qgc-eng-server/pom.xml create mode 100644 backend/qgc-eng-server/src/main/java/com/yfd/platform/eng/controller/EngController.java create mode 100644 backend/qgc-eng-server/src/main/java/com/yfd/platform/eng/service/IEngService.java create mode 100644 backend/qgc-eng-server/src/main/java/com/yfd/platform/eng/service/impl/EngServiceImpl.java create mode 100644 backend/qgc-env-server/pom.xml create mode 100644 backend/qgc-env-server/src/main/java/com/yfd/platform/env/controller/EnvController.java create mode 100644 backend/qgc-env-server/src/main/java/com/yfd/platform/env/service/IEnvService.java create mode 100644 backend/qgc-env-server/src/main/java/com/yfd/platform/env/service/impl/EnvServiceImpl.java diff --git a/backend/platform-common/pom.xml b/backend/platform-common/pom.xml new file mode 100644 index 0000000..d3c7b9f --- /dev/null +++ b/backend/platform-common/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + + com.yfd + platform + 1.0 + ../pom.xml + + + platform-common + 1.0 + jar + + Platform Common + Platform Common Module + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.projectlombok + lombok + true + + + + + cn.hutool + hutool-all + + + + \ No newline at end of file diff --git a/backend/platform-common/src/main/java/com/yfd/platform/common/request/RequestParams.java b/backend/platform-common/src/main/java/com/yfd/platform/common/request/RequestParams.java new file mode 100644 index 0000000..5eabb09 --- /dev/null +++ b/backend/platform-common/src/main/java/com/yfd/platform/common/request/RequestParams.java @@ -0,0 +1,48 @@ +package com.yfd.platform.common.request; + +import lombok.Data; +import java.util.Map; + +/** + * 统一请求参数结构 + */ +@Data +public class RequestParams { + /** + * 分页参数 + */ + private PageParams page; + + /** + * 排序参数 + */ + private SortParams sort; + + /** + * 筛选条件 + */ + private Map filter; + + /** + * 业务参数 + */ + private Map params; + + /** + * 分页参数类 + */ + @Data + public static class PageParams { + private int pageSize; + private int pageNumber; + } + + /** + * 排序参数类 + */ + @Data + public static class SortParams { + private String field; + private String direction; + } +} \ No newline at end of file diff --git a/backend/platform-common/src/main/java/com/yfd/platform/common/response/ResponseResult.java b/backend/platform-common/src/main/java/com/yfd/platform/common/response/ResponseResult.java new file mode 100644 index 0000000..4f65bf8 --- /dev/null +++ b/backend/platform-common/src/main/java/com/yfd/platform/common/response/ResponseResult.java @@ -0,0 +1,60 @@ +package com.yfd.platform.common.response; + +import java.util.HashMap; + +/** + * 统一返回结构 + */ +public class ResponseResult extends HashMap { + private static final long serialVersionUID = 1L; + + public ResponseResult() { + } + + public static ResponseResult unlogin() { + return message("401", "未登录"); + } + + public static ResponseResult error() { + return error("操作失败"); + } + + public static ResponseResult success() { + return success("操作成功"); + } + + public static ResponseResult error(String msg) { + ResponseResult json = new ResponseResult(); + json.put((String)"code", "1");//错误 + json.put((String)"msg", msg); + return json; + } + + public static ResponseResult message(String code, String msg) { + ResponseResult json = new ResponseResult(); + json.put((String)"code", code); + json.put((String)"msg", msg); + return json; + } + + public static ResponseResult success(String msg) { + ResponseResult json = new ResponseResult(); + json.put((String)"code", "0");//正常 + json.put((String)"msg", msg); + return json; + } + + public static ResponseResult successData(Object obj) { + ResponseResult json = new ResponseResult(); + json.put((String)"code", "0");//正常 + json.put((String)"msg", "操作成功"); + json.put("data", obj); + return json; + } + + + public ResponseResult put(String key, Object value) { + super.put(key, value); + return this; + } +} \ No newline at end of file diff --git a/backend/platform-common/src/main/java/com/yfd/platform/common/utils/CallBack.java b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/CallBack.java new file mode 100644 index 0000000..8051fc7 --- /dev/null +++ b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/CallBack.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yfd.platform.common.utils; + +import com.yfd.platform.system.utils.SpringContextHolder; + +/** + * @author: liaojinlong + * @date: 2020/6/9 17:02 + * @since: 1.0 + * @see {@link SpringContextHolder} + * 针对某些初始化方法,在SpringContextHolder 初始化前时,
+ * 可提交一个 提交回调任务。
+ * 在SpringContextHolder 初始化后,进行回调使用 + */ + +public interface CallBack { + /** + * 回调执行方法 + */ + void executor(); + + /** + * 本回调任务名称 + * @return / + */ + default String getCallBackName() { + return Thread.currentThread().getId() + ":" + this.getClass().getName(); + } +} + diff --git a/backend/platform-common/src/main/java/com/yfd/platform/common/utils/CodeGenerator.java b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/CodeGenerator.java new file mode 100644 index 0000000..e1b95e9 --- /dev/null +++ b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/CodeGenerator.java @@ -0,0 +1,75 @@ +package com.yfd.platform.common.utils; + +import com.baomidou.mybatisplus.generator.FastAutoGenerator; +import com.baomidou.mybatisplus.generator.config.OutputFile; +import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; +import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine; + +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; + +// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中 +public class CodeGenerator { + + public static String scanner(String tip) { + Scanner scanner = new Scanner(System.in); + StringBuilder help = new StringBuilder(); + help.append("请输入" + tip + ":"); + System.out.println(help.toString()); + if (scanner.hasNext()) { + String ipt = scanner.next(); + if (ipt != null && !ipt.trim().isEmpty()) { + return ipt; + } + } + throw new RuntimeException("请输入正确的" + tip + "!"); + } + + public static void main(String[] args) { + String projectPath = System.getProperty("user.dir"); + String module = scanner("模块名称"); + + Map pathInfo = new HashMap<>(); + pathInfo.put(OutputFile.entity, projectPath + "/src/main/java/com/yfd/platform/" + module + "/domain"); + pathInfo.put(OutputFile.mapper, projectPath + "/src/main/java/com/yfd/platform/" + module + "/mapper"); + pathInfo.put(OutputFile.controller, projectPath + "/src/main/java/com/yfd/platform/" + module + "/controller"); + pathInfo.put(OutputFile.serviceImpl, projectPath + "/src/main/java/com/yfd/platform/" + module + "/service/impl"); + pathInfo.put(OutputFile.service, projectPath + "/src/main/java/com/yfd/platform/" + module + "/service"); + pathInfo.put(OutputFile.xml, projectPath + "/src/main/resources/mapper/" + module); + + FastAutoGenerator.create( + "jdbc:mysql://43.143.220.7:3306/frameworkdb2023?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&autoReconnect=true&failOverReadOnly=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai", + "root", + "zhengg7QkXa { + builder.author("TangWei") + .disableOpenDir() + .outputDir(projectPath + "/src/main/java"); + }) + .packageConfig(builder -> { + builder.parent("com.yfd.platform") + .moduleName(module) + .pathInfo(pathInfo); + }) + .strategyConfig(builder -> { + builder.addInclude(scanner("表名,多个英文逗号分割").split(",")) + .entityBuilder() + .enableLombok() + .naming(NamingStrategy.underline_to_camel) + .columnNaming(NamingStrategy.underline_to_camel) + .controllerBuilder() + .enableRestStyle() + .mapperBuilder() + .formatMapperFileName("%sMapper") + .serviceBuilder() + .formatServiceFileName("%sService") + .formatServiceImplFileName("%sServiceImpl") + .controllerBuilder() + .formatFileName("%sController"); + }) + .templateEngine(new FreemarkerTemplateEngine()) + .execute(); + } + +} diff --git a/backend/platform-common/src/main/java/com/yfd/platform/common/utils/EncryptConfigUtil.java b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/EncryptConfigUtil.java new file mode 100644 index 0000000..e215f1f --- /dev/null +++ b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/EncryptConfigUtil.java @@ -0,0 +1,25 @@ +package com.yfd.platform.common.utils; + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +public class EncryptConfigUtil { + + public static void main(String[] args) { + +// String salt = "rca20230101"; +// String password = "123456"; +// +// BasicTextEncryptor textEncryptor = new BasicTextEncryptor(); +// //加密所需的salt +// textEncryptor.setPassword(salt); +// //要加密的数据(数据库的用户名或密码) +// String encrypt = textEncryptor.encrypt(password); +// System.out.println("password:"+encrypt); + + + BCryptPasswordEncoder passwordEncoder=new BCryptPasswordEncoder(); + String cryptPassword=passwordEncoder.encode("dl_2023");//设置缺省密码 + + } + +} \ No newline at end of file diff --git a/backend/platform-common/src/main/java/com/yfd/platform/common/utils/EncryptUtils.java b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/EncryptUtils.java new file mode 100644 index 0000000..c344c54 --- /dev/null +++ b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/EncryptUtils.java @@ -0,0 +1,100 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.common.utils; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; +import javax.crypto.spec.IvParameterSpec; +import java.nio.charset.StandardCharsets; + +/** + * 加密 + * @author + * @date 2018-11-23 + */ + +public class EncryptUtils { + + private static final String STR_PARAM = "Passw0rd"; + + private static Cipher cipher; + + private static final IvParameterSpec IV = new IvParameterSpec(STR_PARAM.getBytes(StandardCharsets.UTF_8)); + + private static DESKeySpec getDesKeySpec(String source) throws Exception { + if (source == null || source.length() == 0){ + return null; + } + cipher = Cipher.getInstance("DES/CBC/PKCS5Padding"); + String strKey = "Passw0rd"; + return new DESKeySpec(strKey.getBytes(StandardCharsets.UTF_8)); + } + + /** + * 对称加密 + */ + public static String desEncrypt(String source) throws Exception { + DESKeySpec desKeySpec = getDesKeySpec(source); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); + SecretKey secretKey = keyFactory.generateSecret(desKeySpec); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, IV); + return byte2hex( + cipher.doFinal(source.getBytes(StandardCharsets.UTF_8))).toUpperCase(); + } + + /** + * 对称解密 + */ + public static String desDecrypt(String source) throws Exception { + byte[] src = hex2byte(source.getBytes(StandardCharsets.UTF_8)); + DESKeySpec desKeySpec = getDesKeySpec(source); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); + SecretKey secretKey = keyFactory.generateSecret(desKeySpec); + cipher.init(Cipher.DECRYPT_MODE, secretKey, IV); + byte[] retByte = cipher.doFinal(src); + return new String(retByte); + } + + private static String byte2hex(byte[] inStr) { + String stmp; + StringBuilder out = new StringBuilder(inStr.length * 2); + for (byte b : inStr) { + stmp = Integer.toHexString(b & 0xFF); + if (stmp.length() == 1) { + // 如果是0至F的单位字符串,则添加0 + out.append("0").append(stmp); + } else { + out.append(stmp); + } + } + return out.toString(); + } + + private static byte[] hex2byte(byte[] b) { + int size = 2; + if ((b.length % size) != 0){ + throw new IllegalArgumentException("长度不是偶数"); + } + byte[] b2 = new byte[b.length / 2]; + for (int n = 0; n < b.length; n += size) { + String item = new String(b, n, 2); + b2[n / 2] = (byte) Integer.parseInt(item, 16); + } + return b2; + } +} diff --git a/backend/platform-common/src/main/java/com/yfd/platform/common/utils/ExecutionJob.java b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/ExecutionJob.java new file mode 100644 index 0000000..b73b013 --- /dev/null +++ b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/ExecutionJob.java @@ -0,0 +1,102 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.common.utils; + +import com.yfd.platform.config.MessageConfig; +import com.yfd.platform.config.thread.ThreadPoolExecutorUtil; +import com.yfd.platform.system.domain.Message; +import com.yfd.platform.system.domain.QuartzJob; +import com.yfd.platform.system.service.IMessageService; +import com.yfd.platform.system.service.IQuartzJobService; +import jakarta.annotation.Resource; +import org.quartz.JobExecutionContext; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.quartz.QuartzJobBean; + +import java.sql.Timestamp; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 参考人人开源,https://gitee.com/renrenio/renren-security + * + * @author / + * @date 2019-01-07 + */ +@Async +@SuppressWarnings({"unchecked", "all"}) +public class ExecutionJob extends QuartzJobBean { + + /** + * 该处仅供参考 + */ + private final static ThreadPoolExecutor EXECUTOR = + ThreadPoolExecutorUtil.getPoll(); + + @Resource + private IMessageService messageService; + + @Resource + private MessageConfig messageConfig; + + @Override + public void executeInternal(JobExecutionContext context) { + QuartzJob quartzJob = + (QuartzJob) context.getMergedJobDataMap().get(QuartzJob.JOB_KEY); + // 获取spring bean + IQuartzJobService quartzJobService = + SpringContextHolder.getBean(IQuartzJobService.class); + String uuid = quartzJob.getId(); + long startTime = System.currentTimeMillis(); + String jobName = quartzJob.getJobName(); + try { + // 执行任务 + System.out.println( + "--------------------------------------------------------------"); + System.out.println("任务开始执行,任务名称:" + jobName); + QuartzRunnable task = new QuartzRunnable(quartzJob.getJobClass(), + quartzJob.getJobMethod(), + quartzJob.getJobParams()); + Future future = EXECUTOR.submit(task); + future.get(); + long times = System.currentTimeMillis() - startTime; + Message message = new Message(); + message.setCreatetime(new Timestamp(System.currentTimeMillis())); + message.setType("1"); + message.setTitle(quartzJob.getJobName()); + message.setContent(quartzJob.getDescription()); + message.setSenderName("定时器"); + message.setReceiverCodes(quartzJob.getOrderno().toString()); + message.setReceiverNames(""); + message.setStatus("1"); + message.setValidperiod(24); + messageConfig.addMessage(message); + // 任务状态 + System.out.println("任务执行完毕,任务名称:" + jobName + ", " + + "执行时间:" + times + "毫秒"); + System.out.println( + "--------------------------------------------------------------"); + } catch (Exception e) { + System.out.println("任务执行失败,任务名称:" + jobName); + System.out.println( + "--------------------------------------------------------------"); + quartzJob.setStatus("0"); + //更新状态 + quartzJobService.updateById(quartzJob); + } + } + +} diff --git a/backend/platform-common/src/main/java/com/yfd/platform/common/utils/FileUtil.java b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/FileUtil.java new file mode 100644 index 0000000..91882ad --- /dev/null +++ b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/FileUtil.java @@ -0,0 +1,395 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.common.utils; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.poi.excel.BigExcelWriter; +import cn.hutool.poi.excel.ExcelUtil; +import com.yfd.platform.exception.BadRequestException; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.poi.util.IOUtils; +import org.apache.poi.xssf.streaming.SXSSFSheet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.multipart.MultipartFile; + +import java.io.*; +import java.net.URLDecoder; +import java.security.MessageDigest; +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * File工具类,扩展 hutool 工具包 + * + * @author + * @date 2018-12-27 + */ +public class FileUtil extends cn.hutool.core.io.FileUtil { + + private static final Logger log = LoggerFactory.getLogger(FileUtil.class); + + /** + * 系统临时目录 + *
+ * windows 包含路径分割符,但Linux 不包含, + * 在windows \\==\ 前提下, + * 为安全起见 同意拼装 路径分割符, + *
+     *       java.io.tmpdir
+     *       windows : C:\Users/xxx\AppData\Local\Temp\
+     *       linux: /temp
+     * 
+ */ + public static final String SYS_TEM_DIR = System.getProperty("java.io.tmpdir") + File.separator; + /** + * 定义GB的计算常量 + */ + private static final int GB = 1024 * 1024 * 1024; + /** + * 定义MB的计算常量 + */ + private static final int MB = 1024 * 1024; + /** + * 定义KB的计算常量 + */ + private static final int KB = 1024; + + /** + * 格式化小数 + */ + private static final DecimalFormat DF = new DecimalFormat("0.00"); + + public static final String IMAGE = "image"; + public static final String TXT = "document"; + public static final String MUSIC = "music"; + public static final String VIDEO = "video"; + public static final String OTHER = "other"; + + + /** + * MultipartFile转File + */ + public static File toFile(MultipartFile multipartFile) { + // 获取文件名 + String fileName = multipartFile.getOriginalFilename(); + // 获取文件后缀 + String prefix = "." + getExtensionName(fileName); + File file = null; + try { + // 用uuid作为文件名,防止生成的临时文件重复 + file = File.createTempFile(IdUtil.simpleUUID(), prefix); + // MultipartFile to File + multipartFile.transferTo(file); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + return file; + } + + /** + * 获取文件扩展名,不带 . + */ + public static String getExtensionName(String filename) { + if ((filename != null) && (filename.length() > 0)) { + int dot = filename.lastIndexOf('.'); + if ((dot > -1) && (dot < (filename.length() - 1))) { + return filename.substring(dot + 1); + } + } + return filename; + } + + /** + * Java文件操作 获取不带扩展名的文件名 + */ + public static String getFileNameNoEx(String filename) { + if ((filename != null) && (filename.length() > 0)) { + int dot = filename.lastIndexOf('.'); + if ((dot > -1) && (dot < (filename.length()))) { + return filename.substring(0, dot); + } + } + return filename; + } + + /** + * 文件大小转换 + */ + public static String getSize(long size) { + String resultSize; + if (size / GB >= 1) { + //如果当前Byte的值大于等于1GB + resultSize = DF.format(size / (float) GB) + "GB "; + } else if (size / MB >= 1) { + //如果当前Byte的值大于等于1MB + resultSize = DF.format(size / (float) MB) + "MB "; + } else if (size / KB >= 1) { + //如果当前Byte的值大于等于1KB + resultSize = DF.format(size / (float) KB) + "KB "; + } else { + resultSize = size + "B "; + } + return resultSize; + } + + /** + * inputStream 转 File + */ + static File inputStreamToFile(InputStream ins, String name) throws Exception { + File file = new File(SYS_TEM_DIR + name); + if (file.exists()) { + return file; + } + OutputStream os = new FileOutputStream(file); + int bytesRead; + int len = 8192; + byte[] buffer = new byte[len]; + while ((bytesRead = ins.read(buffer, 0, len)) != -1) { + os.write(buffer, 0, bytesRead); + } + os.close(); + ins.close(); + return file; + } + + /** + * 将文件名解析成文件的上传路径 + */ + public static File upload(MultipartFile file, String filePath) { + Date date = new Date(); + SimpleDateFormat format = new SimpleDateFormat("yyyyMMddhhmmssS"); + String name = getFileNameNoEx(file.getOriginalFilename()); + String suffix = getExtensionName(file.getOriginalFilename()); + String nowStr = "-" + format.format(date); + try { + String fileName = name + "." + suffix; + String path = filePath +File.separator + fileName; + // getCanonicalFile 可解析正确各种路径 + File dest = new File(path).getCanonicalFile(); + // 检测是否存在目录 + if (!dest.getParentFile().exists()) { + if (!dest.getParentFile().mkdirs()) { + System.out.println("was not successful."); + } + } + // 文件写入 + file.transferTo(dest); + return dest; + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + /** + * 将文件名解析成文件的上传路径 + * file 上传的文件 + * filePath 存储路径 + * tofilename 保存文件名称 + + */ + public static File upload(MultipartFile file, String filePath,String tofilename) { + try { + String filename = filePath + File.separator + tofilename; + File dest = new File(filename).getCanonicalFile(); + // 检测是否存在目录 + if (!dest.getParentFile().exists()) { + if (!dest.getParentFile().mkdirs()) { + } + } + // 文件写入 + file.transferTo(dest); + return dest; + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + + /** + * 导出excel + */ + public static void downloadExcel(List> list, HttpServletResponse response) throws IOException { + String tempPath = SYS_TEM_DIR + IdUtil.fastSimpleUUID() + ".xlsx"; + String filename = "record"+cn.hutool.core.date.DateUtil.format(DateUtil.date(), "yyyyMMddHHmmss"); + File file = new File(tempPath); + BigExcelWriter writer = ExcelUtil.getBigWriter(file); + // 一次性写出内容,使用默认样式,强制输出标题 + writer.write(list, true); + SXSSFSheet sheet = (SXSSFSheet)writer.getSheet(); + //上面需要强转SXSSFSheet 不然没有trackAllColumnsForAutoSizing方法 + sheet.trackAllColumnsForAutoSizing(); + //列宽自适应 + writer.autoSizeColumnAll(); + //response为HttpServletResponse对象 + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"); + //test.xls是弹出下载对话框的文件名,不能为中文,中文请自行编码 + response.setHeader("Content-Disposition", "attachment;filename="+filename+".xlsx"); + ServletOutputStream out = response.getOutputStream(); + // 终止后删除临时文件 + file.deleteOnExit(); + writer.flush(out, true); + //此处记得关闭输出Servlet流 + IoUtil.close(out); + } + + public static String getFileType(String type) { + String documents = "txt doc pdf ppt pps xlsx xls docx"; + String music = "mp3 wav wma mpa ram ra aac aif m4a"; + String video = "avi mpg mpe mpeg asf wmv mov qt rm mp4 flv m4v webm ogv ogg"; + String image = "bmp dib pcp dif wmf gif jpg tif eps psd cdr iff tga pcd mpt png jpeg"; + if (image.contains(type)) { + return IMAGE; + } else if (documents.contains(type)) { + return TXT; + } else if (music.contains(type)) { + return MUSIC; + } else if (video.contains(type)) { + return VIDEO; + } else { + return OTHER; + } + } + + public static void checkSize(long maxSize, long size) { + // 1M + int len = 1024 * 1024; + if (size > (maxSize * len)) { + throw new BadRequestException("文件超出规定大小"); + } + } + + /** + * 判断两个文件是否相同 + */ + public static boolean check(File file1, File file2) { + String img1Md5 = getMd5(file1); + String img2Md5 = getMd5(file2); + return img1Md5.equals(img2Md5); + } + + /** + * 判断两个文件是否相同 + */ + public static boolean check(String file1Md5, String file2Md5) { + return file1Md5.equals(file2Md5); + } + + private static byte[] getByte(File file) { + // 得到文件长度 + byte[] b = new byte[(int) file.length()]; + try { + InputStream in = new FileInputStream(file); + try { + System.out.println(in.read(b)); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } catch (FileNotFoundException e) { + log.error(e.getMessage(), e); + return null; + } + return b; + } + + private static String getMd5(byte[] bytes) { + // 16进制字符 + char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + try { + MessageDigest mdTemp = MessageDigest.getInstance("MD5"); + mdTemp.update(bytes); + byte[] md = mdTemp.digest(); + int j = md.length; + char[] str = new char[j * 2]; + int k = 0; + // 移位 输出字符串 + for (byte byte0 : md) { + str[k++] = hexDigits[byte0 >>> 4 & 0xf]; + str[k++] = hexDigits[byte0 & 0xf]; + } + return new String(str); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + /** + * 下载文件 + * + * @param request / + * @param response / + * @param file / + */ + public static void downloadFile(HttpServletRequest request, HttpServletResponse response, File file, boolean deleteOnExit) { + response.setCharacterEncoding(request.getCharacterEncoding()); + response.setContentType("application/octet-stream"); + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + response.setHeader("Content-Disposition", "attachment; filename=" + file.getName()); + IOUtils.copy(fis, response.getOutputStream()); + response.flushBuffer(); + } catch (Exception e) { + log.error(e.getMessage(), e); + } finally { + if (fis != null) { + try { + fis.close(); + if (deleteOnExit) { + file.deleteOnExit(); + } + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + } + } + /** + * 预览PDF文件 + * + * @param filepath / + * @param response / + */ + public static void viewPDF(String filepath, HttpServletResponse response) throws IOException { + File file=new File(filepath); + String originFileName=file.getName(); //中文编码 + response.setCharacterEncoding("UTF-8"); + String showName= StrUtil.isNotBlank(originFileName)?originFileName:file.getName(); + showName= URLDecoder.decode(showName,"UTF-8"); + response.setHeader("Content-Disposition","inline;fileName="+new String(showName.getBytes(), "ISO8859-1")+";fileName*=UTF-8''"+ new String(showName.getBytes(), "ISO8859-1")); + FileInputStream fis = new FileInputStream(file); + response.setHeader("content-type", "application/pdf"); + response.setContentType("application/pdf; charset=utf-8"); + IOUtils.copy(fis, response.getOutputStream()); + fis.close(); + } + + public static String getMd5(File file) { + return getMd5(getByte(file)); + } + +} diff --git a/backend/platform-common/src/main/java/com/yfd/platform/common/utils/MpGenerator.java b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/MpGenerator.java new file mode 100644 index 0000000..9a67175 --- /dev/null +++ b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/MpGenerator.java @@ -0,0 +1,80 @@ +package com.yfd.platform.common.utils; + +import com.baomidou.mybatisplus.generator.FastAutoGenerator; +import com.baomidou.mybatisplus.generator.config.OutputFile; +import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; +import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine; +import com.yfd.platform.system.utils.PropertiesUtils; +import org.springframework.util.StringUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; + +public class MpGenerator { + /** + * 读取控制台内容 + */ + public static String scanner(String tip) { + Scanner scanner = new Scanner(System.in); + StringBuilder help = new StringBuilder(); + help.append("请输入" + tip + ":"); + System.out.println(help.toString()); + if (scanner.hasNext()) { + String ipt = scanner.next(); + if (StringUtils.hasText(ipt)) { + return ipt; + } + } + throw new RuntimeException("请输入正确的" + tip + "!"); + } + + public static void main(String[] args) { + String projectPath = System.getProperty("user.dir"); + String url = PropertiesUtils.getPropertyField("spring.datasource.url"); + String username = PropertiesUtils.getPropertyField("spring.datasource.username"); + String password = PropertiesUtils.getPropertyField("spring.datasource.password"); + + String moduleName = scanner("模块名"); + String modulePath = moduleName.replace(".", "/"); + + Map pathInfo = new HashMap<>(); + pathInfo.put(OutputFile.entity, projectPath + "/src/main/java/com/yfd/platform/modules/domain" + modulePath + "/entity"); + pathInfo.put(OutputFile.mapper, projectPath + "/src/main/java/com/yfd/platform/modules/domain/" + modulePath + "/dao"); + pathInfo.put(OutputFile.controller, projectPath + "/src/main/java/com/yfd/platform/modules/domain" + modulePath + "/controller"); + pathInfo.put(OutputFile.service, projectPath + "/src/main/java/com/yfd/platform/modules/domain" + modulePath + "/service"); + pathInfo.put(OutputFile.serviceImpl, projectPath + "/src/main/java/com/yfd/platform/modules/domain" + modulePath + "/service/impl"); + pathInfo.put(OutputFile.xml, projectPath + "/src/main/resources/mapper/" + modulePath); + + FastAutoGenerator.create(url, username, password) + .globalConfig(builder -> { + builder.author("fwh") + .disableOpenDir() + .outputDir(projectPath + "/src/main/java"); + }) + .packageConfig(builder -> { + builder.parent(PropertiesUtils.getPropertyField("project.package.name")) + .moduleName(moduleName) + .pathInfo(pathInfo); + }) + .strategyConfig(builder -> { + builder.addInclude(scanner("表名,多个英文逗号分割").split(",")) + .entityBuilder() + .enableLombok() + .naming(NamingStrategy.underline_to_camel) + .columnNaming(NamingStrategy.underline_to_camel) + .controllerBuilder() + .enableRestStyle() + .mapperBuilder() + .formatMapperFileName("%sDao") + .serviceBuilder() + .formatServiceFileName("%sService") + .formatServiceImplFileName("%sServiceImpl") + .controllerBuilder() + .formatFileName("%sController"); + }) + .templateEngine(new FreemarkerTemplateEngine()) + .execute(); + } +} + diff --git a/backend/platform-common/src/main/java/com/yfd/platform/common/utils/ObjectConverterUtil.java b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/ObjectConverterUtil.java new file mode 100644 index 0000000..b1117ef --- /dev/null +++ b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/ObjectConverterUtil.java @@ -0,0 +1,152 @@ +package com.yfd.platform.common.utils; + +import java.lang.reflect.Field; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class ObjectConverterUtil { + + // 日期时间格式 + private static final DateTimeFormatter DATE_TIME_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 将 List 中的大写字段名转换为与实体类一致的小写格式 + * 适用于 Oracle 数据库查询结果转换 + * 时间类型字段会转换为字符串格式 + * + * @param entityClass 实体类 Class 对象 + * @param sourceList 源数据列表(字段名为大写) + * @return 转换后的列表(字段名与实体类一致) + */ + public static List> convertMapFieldsToEntityFormat( + Class entityClass, + List> sourceList) { + + if (sourceList == null || sourceList.isEmpty()) { + return sourceList; + } + + // 获取实体类的所有字段及类型 + Map fieldMap = new HashMap<>(); + for (Field field : entityClass.getDeclaredFields()) { + fieldMap.put(field.getName(), field); + } + + // 转换每个 Map + return sourceList.stream().map(sourceMap -> { + Map targetMap = new HashMap<>(); + + for (Map.Entry entry : sourceMap.entrySet()) { + String dbColumnName = entry.getKey(); // 数据库列名(大写) + Object value = entry.getValue(); + + // 将大写列名转为小写 + String lowerCaseName = dbColumnName.toLowerCase(); + + // 如果实体类中有对应的字段 + if (fieldMap.containsKey(lowerCaseName)) { + Field field = fieldMap.get(lowerCaseName); + String fieldType = field.getType().getSimpleName(); + + // 处理时间类型,转换为字符串 + if ("Timestamp".equals(fieldType) || "Date".equals(fieldType) || + "LocalDateTime".equals(fieldType)) { + targetMap.put(lowerCaseName, formatDateTimeValue(value)); + } else { + targetMap.put(lowerCaseName, value); + } + } else { + // 如果实体类中没有对应字段,保留原列名(转小写) + targetMap.put(lowerCaseName, value); + } + } + + return targetMap; + }).collect(Collectors.toList()); + } + + /** + * 通用方法:将单个 Map 的大写字段转换为实体类格式 + * 时间类型字段会转换为字符串格式 + * + * @param entityClass 实体类 Class 对象 + * @param sourceMap 源 Map(字段名为大写) + * @return 转换后的 Map(字段名与实体类一致) + */ + public static Map convertSingleMapFieldsToEntityFormat( + Class entityClass, + Map sourceMap) { + + if (sourceMap == null) { + return sourceMap; + } + + // 获取实体类的所有字段及类型 + Map fieldMap = new HashMap<>(); + for (Field field : entityClass.getDeclaredFields()) { + fieldMap.put(field.getName(), field); + } + + Map targetMap = new HashMap<>(); + + for (Map.Entry entry : sourceMap.entrySet()) { + String dbColumnName = entry.getKey(); + Object value = entry.getValue(); + + // 将大写列名转为小写 + String lowerCaseName = dbColumnName.toLowerCase(); + + // 验证字段是否存在于实体类中 + if (fieldMap.containsKey(lowerCaseName)) { + Field field = fieldMap.get(lowerCaseName); + String fieldType = field.getType().getSimpleName(); + + // 处理时间类型,转换为字符串 + if ("Timestamp".equals(fieldType) || "Date".equals(fieldType) || + "LocalDateTime".equals(fieldType)) { + targetMap.put(lowerCaseName, formatDateTimeValue(value)); + } else { + targetMap.put(lowerCaseName, value); + } + } + } + + return targetMap; + } + + /** + * 格式化时间类型值为字符串 + * + * @param value 时间值(可能是 Timestamp、Date 或 LocalDateTime) + * @return 格式化后的字符串,如果为 null 则返回 null + */ + private static String formatDateTimeValue(Object value) { + if (value == null) { + return null; + } + + if (value instanceof Timestamp) { + LocalDateTime localDateTime = ((Timestamp) value).toLocalDateTime(); + return localDateTime.format(DATE_TIME_FORMATTER); + } else if (value instanceof Date) { + // 包括 java.sql.Date 和 java.util.Date + LocalDateTime localDateTime = ((Date) value).toInstant() + .atZone(java.time.ZoneId.systemDefault()) + .toLocalDateTime(); + return localDateTime.format(DATE_TIME_FORMATTER); + } else if (value instanceof LocalDateTime) { + return ((LocalDateTime) value).format(DATE_TIME_FORMATTER); + } else { + // 其他类型直接转字符串 + return value.toString(); + } + } + +} diff --git a/backend/platform-common/src/main/java/com/yfd/platform/common/utils/PropertiesUtils.java b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/PropertiesUtils.java new file mode 100644 index 0000000..2ed6b8a --- /dev/null +++ b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/PropertiesUtils.java @@ -0,0 +1,29 @@ +package com.yfd.platform.common.utils; + +import org.springframework.core.io.ClassPathResource; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Properties; +/****************************** + * 用途说明: + * 作者姓名: pcj + * 创建时间: 2022/9/20 14:31 + ******************************/ +public class PropertiesUtils { + public final static String RESOURCE_PATH = "application.properties"; + + public final static Properties properties = new Properties(); + + public static String getPropertyField(String parameter) { + //对应resources目录下的资源路径 + ClassPathResource resource = new ClassPathResource(RESOURCE_PATH); + try { + properties.load(new InputStreamReader(resource.getInputStream(), "gbk")); + } catch (IOException e) { + throw new RuntimeException(e); + } + return properties.getProperty(parameter); + } + +} diff --git a/backend/platform-common/src/main/java/com/yfd/platform/common/utils/QuartzManage.java b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/QuartzManage.java new file mode 100644 index 0000000..aee53a6 --- /dev/null +++ b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/QuartzManage.java @@ -0,0 +1,187 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.common.utils; + +import com.yfd.platform.system.domain.QuartzJob; +import com.yfd.platform.system.utils.ExecutionJob; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.quartz.impl.triggers.CronTriggerImpl; +import org.springframework.stereotype.Component; + +import java.util.Date; + +import static org.quartz.TriggerBuilder.newTrigger; + +/** + * @author + * @date 2019-01-07 + */ +@Slf4j +@Component +public class QuartzManage { + + private static final String JOB_NAME = "TASK_"; + + @Resource(name = "scheduler") + private Scheduler scheduler; + + public void addJob(QuartzJob quartzJob) { + try { + // 构建job信息 + JobDetail jobDetail = JobBuilder.newJob(ExecutionJob.class). + withIdentity(JOB_NAME + quartzJob.getId()).build(); + + //通过触发器名和cron 表达式创建 Trigger + Trigger cronTrigger = newTrigger() + .withIdentity(JOB_NAME + quartzJob.getId()) + .startNow() + .withSchedule(CronScheduleBuilder.cronSchedule(quartzJob.getJobCron())) + .build(); + + cronTrigger.getJobDataMap().put(QuartzJob.JOB_KEY, quartzJob); + + //重置启动时间 + ((CronTriggerImpl) cronTrigger).setStartTime(new Date()); + + //执行定时任务 + scheduler.scheduleJob(jobDetail, cronTrigger); + + // 暂停任务 + if ("0".equals(quartzJob.getStatus())) { + pauseJob(quartzJob); + } + } catch (Exception e) { + log.error("创建定时任务失败", e); + throw new RuntimeException("创建定时任务失败"); + } + } + + /** + * 更新job cron表达式 + * + * @param quartzJob / + */ + public void updateJobCron(QuartzJob quartzJob) { + try { + TriggerKey triggerKey = + TriggerKey.triggerKey(JOB_NAME + quartzJob.getId()); + CronTrigger trigger = + (CronTrigger) scheduler.getTrigger(triggerKey); + // 如果不存在则创建一个定时任务 + if (trigger == null) { + addJob(quartzJob); + trigger = (CronTrigger) scheduler.getTrigger(triggerKey); + } + CronScheduleBuilder scheduleBuilder = + CronScheduleBuilder.cronSchedule(quartzJob.getJobCron()); + trigger = + trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build(); + //重置启动时间 + ((CronTriggerImpl) trigger).setStartTime(new Date()); + trigger.getJobDataMap().put(QuartzJob.JOB_KEY, quartzJob); + + scheduler.rescheduleJob(triggerKey, trigger); + // 暂停任务 + if ("0".equals(quartzJob.getStatus())) { + pauseJob(quartzJob); + } + } catch (Exception e) { + log.error("更新定时任务失败", e); + throw new RuntimeException("更新定时任务失败"); + } + + } + + /** + * 删除一个job + * + * @param quartzJob / + */ + public void deleteJob(QuartzJob quartzJob) { + try { + JobKey jobKey = JobKey.jobKey(JOB_NAME + quartzJob.getId()); + scheduler.pauseJob(jobKey); + scheduler.deleteJob(jobKey); + } catch (Exception e) { + log.error("删除定时任务失败", e); + throw new RuntimeException("删除定时任务失败"); + } + } + + /** + * 恢复一个job + * + * @param quartzJob / + */ + public void resumeJob(QuartzJob quartzJob) { + try { + TriggerKey triggerKey = + TriggerKey.triggerKey(JOB_NAME + quartzJob.getId()); + CronTrigger trigger = + (CronTrigger) scheduler.getTrigger(triggerKey); + // 如果不存在则创建一个定时任务 + if (trigger == null) { + addJob(quartzJob); + } + JobKey jobKey = JobKey.jobKey(JOB_NAME + quartzJob.getId()); + scheduler.resumeJob(jobKey); + } catch (Exception e) { + log.error("恢复定时任务失败", e); + throw new RuntimeException("恢复定时任务失败"); + } + } + + /** + * 立即执行job + * + * @param quartzJob / + */ + public void runJobNow(QuartzJob quartzJob) { + try { + TriggerKey triggerKey = + TriggerKey.triggerKey(JOB_NAME + quartzJob.getId()); + CronTrigger trigger = + (CronTrigger) scheduler.getTrigger(triggerKey); + // 如果不存在则创建一个定时任务 + if (trigger == null) { + addJob(quartzJob); + } + JobDataMap dataMap = new JobDataMap(); + dataMap.put(QuartzJob.JOB_KEY, quartzJob); + JobKey jobKey = JobKey.jobKey(JOB_NAME + quartzJob.getId()); + scheduler.triggerJob(jobKey, dataMap); + } catch (Exception e) { + log.error("定时任务执行失败", e); + throw new RuntimeException("定时任务执行失败"); + } + } + + /** + * 暂停一个job + * + * @param quartzJob / + */ + public void pauseJob(QuartzJob quartzJob) { + try { + JobKey jobKey = JobKey.jobKey(JOB_NAME + quartzJob.getId()); + scheduler.pauseJob(jobKey); + } catch (Exception e) { + log.error("定时任务暂停失败", e); + throw new RuntimeException("定时任务暂停失败"); + } + } +} diff --git a/backend/platform-common/src/main/java/com/yfd/platform/common/utils/QuartzRunnable.java b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/QuartzRunnable.java new file mode 100644 index 0000000..354902a --- /dev/null +++ b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/QuartzRunnable.java @@ -0,0 +1,59 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.common.utils; + +import com.yfd.platform.system.utils.SpringContextHolder; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Method; +import java.util.concurrent.Callable; + +/** + * 执行定时任务 + * @author / + */ +@Slf4j +public class QuartzRunnable implements Callable { + + private final Object target; + private final Method method; + private final String params; + + QuartzRunnable(String beanName, String methodName, String params) + throws NoSuchMethodException, SecurityException { + this.target = SpringContextHolder.getBean(beanName); + this.params = params; + + if (StringUtils.isNotBlank(params)) { + this.method = target.getClass().getDeclaredMethod(methodName, String.class); + } else { + this.method = target.getClass().getDeclaredMethod(methodName); + } + } + + @Override + public Object call() throws Exception { + ReflectionUtils.makeAccessible(method); + if (StringUtils.isNotBlank(params)) { + method.invoke(target, params); + } else { + method.invoke(target); + } + return null; + } +} diff --git a/backend/platform-common/src/main/java/com/yfd/platform/common/utils/RequestHolder.java b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/RequestHolder.java new file mode 100644 index 0000000..0d20a96 --- /dev/null +++ b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/RequestHolder.java @@ -0,0 +1,34 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.common.utils; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.util.Objects; + +/** + * 获取 HttpServletRequest + * @author + * @date 2018-11-24 + */ +public class RequestHolder { + + public static HttpServletRequest getHttpServletRequest() { + return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); + } +} diff --git a/backend/platform-common/src/main/java/com/yfd/platform/common/utils/RsaUtils.java b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/RsaUtils.java new file mode 100644 index 0000000..64240eb --- /dev/null +++ b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/RsaUtils.java @@ -0,0 +1,190 @@ +package com.yfd.platform.common.utils; + +import org.apache.commons.codec.binary.Base64; + +import javax.crypto.Cipher; +import java.security.*; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +/** + * @author https://www.cnblogs.com/nihaorz/p/10690643.html + * @description Rsa 工具类,公钥私钥生成,加解密 + * @date 2020-05-18 + **/ +public class RsaUtils { + + private static final String SRC = "123456"; + + public static void main(String[] args) throws Exception { + System.out.println("\n"); +// RsaKeyPair keyPair = generateKeyPair(); + String publicKey = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANL378k3RiZHWx5AfJqdH9xRNBmD9wGD2iRe41HdTNF8RUhNnHit5NpMNtGL0NPTSSpPjjI1kJfVorRvaQerUgkCAwEAAQ=="; + String privateKey = "MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEA0vfvyTdGJkdbHkB8\n" + + "mp0f3FE0GYP3AYPaJF7jUd1M0XxFSE2ceK3k2kw20YvQ09NJKk+OMjWQl9WitG9p\n" + + "B6tSCQIDAQABAkA2SimBrWC2/wvauBuYqjCFwLvYiRYqZKThUS3MZlebXJiLB+Ue\n" + + "/gUifAAKIg1avttUZsHBHrop4qfJCwAI0+YRAiEA+W3NK/RaXtnRqmoUUkb59zsZ\n" + + "UBLpvZgQPfj1MhyHDz0CIQDYhsAhPJ3mgS64NbUZmGWuuNKp5coY2GIj/zYDMJp6\n" + + "vQIgUueLFXv/eZ1ekgz2Oi67MNCk5jeTF2BurZqNLR3MSmUCIFT3Q6uHMtsB9Eha\n" + + "4u7hS31tj1UWE+D+ADzp59MGnoftAiBeHT7gDMuqeJHPL4b+kC+gzV4FGTfhR9q3\n" + + "tTbklZkD2A=="; + RsaKeyPair keyPair =new RsaKeyPair(publicKey, privateKey); + System.out.println("私钥:" + keyPair.getPrivateKey()); + System.out.println("\n"); + test1(keyPair); + System.out.println("\n"); +// test2(keyPair); + System.out.println("\n"); + } + + /** + * 公钥加密私钥解密 + */ + private static void test1(RsaKeyPair keyPair) throws Exception { + System.out.println("***************** 公钥加密私钥解密开始 *****************"); + String text1 = encryptByPublicKey(keyPair.getPublicKey(), RsaUtils.SRC); + String text2 = decryptByPrivateKey(keyPair.getPrivateKey(), text1); + System.out.println("加密前:" + RsaUtils.SRC); + System.out.println("加密后:" + text1); + System.out.println("解密后:" + text2); + if (RsaUtils.SRC.equals(text2)) { + System.out.println("解密字符串和原始字符串一致,解密成功"); + } else { + System.out.println("解密字符串和原始字符串不一致,解密失败"); + } + System.out.println("***************** 公钥加密私钥解密结束 *****************"); + } + + /** + * 私钥加密公钥解密 + * @throws Exception / + */ + private static void test2(RsaKeyPair keyPair) throws Exception { + System.out.println("***************** 私钥加密公钥解密开始 *****************"); + String text1 = encryptByPrivateKey(keyPair.getPrivateKey(), RsaUtils.SRC); + String text2 = decryptByPublicKey(keyPair.getPublicKey(), text1); + System.out.println("加密前:" + RsaUtils.SRC); + System.out.println("加密后:" + text1); + System.out.println("解密后:" + text2); + if (RsaUtils.SRC.equals(text2)) { + System.out.println("解密字符串和原始字符串一致,解密成功"); + } else { + System.out.println("解密字符串和原始字符串不一致,解密失败"); + } + System.out.println("***************** 私钥加密公钥解密结束 *****************"); + } + + /** + * 公钥解密 + * + * @param publicKeyText 公钥 + * @param text 待解密的信息 + * @return / + * @throws Exception / + */ + public static String decryptByPublicKey(String publicKeyText, String text) throws Exception { + X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyText)); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec); + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.DECRYPT_MODE, publicKey); + byte[] result = cipher.doFinal(Base64.decodeBase64(text)); + return new String(result); + } + + /** + * 私钥加密 + * + * @param privateKeyText 私钥 + * @param text 待加密的信息 + * @return / + * @throws Exception / + */ + public static String encryptByPrivateKey(String privateKeyText, String text) throws Exception { + PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyText)); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec); + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.ENCRYPT_MODE, privateKey); + byte[] result = cipher.doFinal(text.getBytes()); + return Base64.encodeBase64String(result); + } + + /** + * 私钥解密 + * + * @param privateKeyText 私钥 + * @param text 待解密的文本 + * @return / + * @throws Exception / + */ + public static String decryptByPrivateKey(String privateKeyText, String text) throws Exception { + PKCS8EncodedKeySpec pkcs8EncodedKeySpec5 = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyText)); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec5); + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + byte[] result = cipher.doFinal(Base64.decodeBase64(text)); + return new String(result); + } + + /** + * 公钥加密 + * + * @param publicKeyText 公钥 + * @param text 待加密的文本 + * @return / + */ + public static String encryptByPublicKey(String publicKeyText, String text) throws Exception { + X509EncodedKeySpec x509EncodedKeySpec2 = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyText)); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec2); + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + byte[] result = cipher.doFinal(text.getBytes()); + return Base64.encodeBase64String(result); + } + + /** + * 构建RSA密钥对 + * + * @return / + * @throws NoSuchAlgorithmException / + */ + public static RsaKeyPair generateKeyPair() throws NoSuchAlgorithmException { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(1024); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate(); + String publicKeyString = Base64.encodeBase64String(rsaPublicKey.getEncoded()); + String privateKeyString = Base64.encodeBase64String(rsaPrivateKey.getEncoded()); + return new RsaKeyPair(publicKeyString, privateKeyString); + } + + + /** + * RSA密钥对对象 + */ + public static class RsaKeyPair { + + private final String publicKey; + private final String privateKey; + + public RsaKeyPair(String publicKey, String privateKey) { + this.publicKey = publicKey; + this.privateKey = privateKey; + } + + public String getPublicKey() { + return publicKey; + } + + public String getPrivateKey() { + return privateKey; + } + + } +} diff --git a/backend/platform-common/src/main/java/com/yfd/platform/common/utils/SecurityUtils.java b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/SecurityUtils.java new file mode 100644 index 0000000..cf4ed72 --- /dev/null +++ b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/SecurityUtils.java @@ -0,0 +1,85 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.common.utils; + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.yfd.platform.exception.BadRequestException; +import com.yfd.platform.system.utils.SpringContextHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; + +import java.util.List; + +/** + * 获取当前登录的用户 + * @author + * @date 2019-01-17 + */ +@Slf4j +public class SecurityUtils { + + /** + * 获取当前登录的用户 + * @return UserDetails + */ + public static UserDetails getCurrentUser() { + UserDetailsService userDetailsService = SpringContextHolder.getBean(UserDetailsService.class); + return userDetailsService.loadUserByUsername(getCurrentUsername()); + } + + /** + * 获取系统用户名称 + * + * @return 系统用户名称 + */ + public static String getCurrentUsername() { + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + throw new BadRequestException(HttpStatus.UNAUTHORIZED, "当前登录状态过期"); + } + if (authentication.getPrincipal() instanceof UserDetails) { + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + return userDetails.getUsername(); + } + throw new BadRequestException(HttpStatus.UNAUTHORIZED, "找不到当前登录的信息"); + } + + /** + * 获取系统用户ID + * @return 系统用户ID + */ + public static Long getCurrentUserId() { + UserDetails userDetails = getCurrentUser(); + return new JSONObject(new JSONObject(userDetails).get("user")).get("id", Long.class); + } + + /** + * 获取当前用户的数据权限 + * @return / + */ + public static List getCurrentUserDataScope(){ + UserDetails userDetails = getCurrentUser(); + JSONArray array = JSONUtil.parseArray(new JSONObject(userDetails).get("dataScopes")); + return JSONUtil.toList(array,Long.class); + } + +} diff --git a/backend/platform-common/src/main/java/com/yfd/platform/common/utils/SpringContextHolder.java b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/SpringContextHolder.java new file mode 100644 index 0000000..29f58ef --- /dev/null +++ b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/SpringContextHolder.java @@ -0,0 +1,145 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.common.utils; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.env.Environment; + +import java.util.ArrayList; +import java.util.List; +/** + * @author Jie + * @date 2019-01-07 + */ +@Slf4j +public class SpringContextHolder implements ApplicationContextAware, DisposableBean { + + private static ApplicationContext applicationContext = null; + private static final List CALL_BACKS = new ArrayList<>(); + private static boolean addCallback = true; + + /** + * 针对 某些初始化方法,在SpringContextHolder 未初始化时 提交回调方法。 + * 在SpringContextHolder 初始化后,进行回调使用 + * + * @param callBack 回调函数 + */ + public synchronized static void addCallBacks(CallBack callBack) { + if (addCallback) { + SpringContextHolder.CALL_BACKS.add(callBack); + } else { + log.warn("CallBack:{} 已无法添加!立即执行", callBack.getCallBackName()); + callBack.executor(); + } + } + + /** + * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型. + */ + @SuppressWarnings("unchecked") + public static T getBean(String name) { + assertContextInjected(); + return (T) applicationContext.getBean(name); + } + + /** + * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型. + */ + public static T getBean(Class requiredType) { + assertContextInjected(); + return applicationContext.getBean(requiredType); + } + + /** + * 获取SpringBoot 配置信息 + * + * @param property 属性key + * @param defaultValue 默认值 + * @param requiredType 返回类型 + * @return / + */ + public static T getProperties(String property, T defaultValue, Class requiredType) { + T result = defaultValue; + try { + result = getBean(Environment.class).getProperty(property, requiredType); + } catch (Exception ignored) {} + return result; + } + + /** + * 获取SpringBoot 配置信息 + * + * @param property 属性key + * @return / + */ + public static String getProperties(String property) { + return getProperties(property, null, String.class); + } + + /** + * 获取SpringBoot 配置信息 + * + * @param property 属性key + * @param requiredType 返回类型 + * @return / + */ + public static T getProperties(String property, Class requiredType) { + return getProperties(property, null, requiredType); + } + + /** + * 检查ApplicationContext不为空. + */ + private static void assertContextInjected() { + if (applicationContext == null) { + throw new IllegalStateException("applicaitonContext属性未注入, 请在applicationContext" + + ".xml中定义SpringContextHolder或在SpringBoot启动类中注册SpringContextHolder."); + } + } + + /** + * 清除SpringContextHolder中的ApplicationContext为Null. + */ + private static void clearHolder() { + log.debug("清除SpringContextHolder中的ApplicationContext:" + + applicationContext); + applicationContext = null; + } + + @Override + public void destroy() { + SpringContextHolder.clearHolder(); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + if (SpringContextHolder.applicationContext != null) { + log.warn("SpringContextHolder中的ApplicationContext被覆盖, 原有ApplicationContext为:" + SpringContextHolder.applicationContext); + } + SpringContextHolder.applicationContext = applicationContext; + if (addCallback) { + for (CallBack callBack : SpringContextHolder.CALL_BACKS) { + callBack.executor(); + } + CALL_BACKS.clear(); + } + SpringContextHolder.addCallback = false; + } +} diff --git a/backend/platform-common/src/main/java/com/yfd/platform/common/utils/StringUtils.java b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/StringUtils.java new file mode 100644 index 0000000..ca8e3ec --- /dev/null +++ b/backend/platform-common/src/main/java/com/yfd/platform/common/utils/StringUtils.java @@ -0,0 +1,306 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.common.utils; + + +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.yfd.platform.constant.Constant; +import eu.bitwalker.useragentutils.Browser; +import eu.bitwalker.useragentutils.UserAgent; +import jakarta.servlet.http.HttpServletRequest; +import lombok.SneakyThrows; +import org.lionsoul.ip2region.DataBlock; +import org.lionsoul.ip2region.DbConfig; +import org.lionsoul.ip2region.DbSearcher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.ClassPathResource; + +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.UnknownHostException; +import java.util.Calendar; +import java.util.Date; +import java.util.Enumeration; + +/** + * @author + * 字符串工具类, 继承org.apache.commons.lang3.StringUtils类 + */ +public class StringUtils extends org.apache.commons.lang3.StringUtils { + + private static final Logger log = LoggerFactory.getLogger(StringUtils.class); + private static boolean ipLocal = false; + private static File file ; + private static DbConfig config; + private static final char SEPARATOR = '_'; + private static final String UNKNOWN = "unknown"; + + static { + SpringContextHolder.addCallBacks(() -> { + StringUtils.ipLocal = SpringContextHolder.getProperties("ip.local-parsing", false, Boolean.class); + if (ipLocal) { + /* + * 此文件为独享 ,不必关闭 + */ + String path = "ip2region/ip2region.db"; + String name = "ip2region.db"; + try { + config = new DbConfig(); + file = FileUtil.inputStreamToFile(new ClassPathResource(path).getInputStream(), name); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + }); + } + + /** + * 驼峰命名法工具 + * + * @return toCamelCase(" hello_world ") == "helloWorld" + * toCapitalizeCamelCase("hello_world") == "HelloWorld" + * toUnderScoreCase("helloWorld") = "hello_world" + */ + public static String toCamelCase(String s) { + if (s == null) { + return null; + } + + s = s.toLowerCase(); + + StringBuilder sb = new StringBuilder(s.length()); + boolean upperCase = false; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + + if (c == SEPARATOR) { + upperCase = true; + } else if (upperCase) { + sb.append(Character.toUpperCase(c)); + upperCase = false; + } else { + sb.append(c); + } + } + + return sb.toString(); + } + + /** + * 驼峰命名法工具 + * + * @return toCamelCase(" hello_world ") == "helloWorld" + * toCapitalizeCamelCase("hello_world") == "HelloWorld" + * toUnderScoreCase("helloWorld") = "hello_world" + */ + public static String toCapitalizeCamelCase(String s) { + if (s == null) { + return null; + } + s = toCamelCase(s); + return s.substring(0, 1).toUpperCase() + s.substring(1); + } + + /** + * 驼峰命名法工具 + * + * @return toCamelCase(" hello_world ") == "helloWorld" + * toCapitalizeCamelCase("hello_world") == "HelloWorld" + * toUnderScoreCase("helloWorld") = "hello_world" + */ + static String toUnderScoreCase(String s) { + if (s == null) { + return null; + } + + StringBuilder sb = new StringBuilder(); + boolean upperCase = false; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + + boolean nextUpperCase = true; + + if (i < (s.length() - 1)) { + nextUpperCase = Character.isUpperCase(s.charAt(i + 1)); + } + + if ((i > 0) && Character.isUpperCase(c)) { + if (!upperCase || !nextUpperCase) { + sb.append(SEPARATOR); + } + upperCase = true; + } else { + upperCase = false; + } + + sb.append(Character.toLowerCase(c)); + } + + return sb.toString(); + } + + /** + * 获取ip地址 + */ + public static String getIp(HttpServletRequest request) { + String ip = request.getHeader("x-forwarded-for"); + if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + String comma = ","; + String localhost = "127.0.0.1"; + if (ip.contains(comma)) { + ip = ip.split(",")[0]; + } + if (localhost.equals(ip)) { + // 获取本机真正的ip地址 + try { + ip = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error(e.getMessage(), e); + } + } + return ip; + } + + /** + * 根据ip获取详细地址 + */ + @SneakyThrows + public static String getCityInfo(String ip) { + if (ipLocal) { + return getLocalCityInfo(ip); + } else { + return getHttpCityInfo(ip); + } + } + + /** + * 根据ip获取详细地址 + */ + public static String getHttpCityInfo(String ip) { + String host = "202.108.22.5"; + //超时应该在3钞以上 + int timeOut = 3000; + boolean status = false; + try { + status = InetAddress.getByName(host).isReachable(timeOut); + } catch (IOException e) { + e.printStackTrace(); + } + String api =""; + if (status){ + api = HttpUtil.get(String.format(Constant.Url.IP_URL, ip)); + }else { + api = "{\"ip\":\"127.0.0.1\",\"pro\":\"\",\"proCode\":\"999999\",\"city\":\"\",\"cityCode\":\"0\",\"region\":\"\",\"regionCode\":\"0\",\"addr\":\" 局域网\",\"regionNames\":\"\",\"err\":\"noprovince\"}"; + } + JSONObject object = JSONUtil.parseObj(api); + return object.get("addr", String.class); + } + + + /** + * 根据ip获取详细地址 + */ + public static String getLocalCityInfo(String ip) { + try { + DataBlock dataBlock = new DbSearcher(config, file.getPath()) + .binarySearch(ip); + String region = dataBlock.getRegion(); + String address = region.replace("0|", ""); + char symbol = '|'; + if (address.charAt(address.length() - 1) == symbol) { + address = address.substring(0, address.length() - 1); + } + return address.equals(Constant.REGION) ? "内网IP" : address; + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return ""; + } + + public static String getBrowser(HttpServletRequest request) { + UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent")); + Browser browser = userAgent.getBrowser(); + return browser.getName(); + } + + /** + * 获得当天是周几 + */ + public static String getWeekDay() { + String[] weekDays = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; + Calendar cal = Calendar.getInstance(); + cal.setTime(new Date()); + + int w = cal.get(Calendar.DAY_OF_WEEK) - 1; + if (w < 0) { + w = 0; + } + return weekDays[w]; + } + + /** + * 获取当前机器的IP + * + * @return / + */ + public static String getLocalIp() { + try { + InetAddress candidateAddress = null; + // 遍历所有的网络接口 + for (Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); interfaces.hasMoreElements();) { + NetworkInterface anInterface = interfaces.nextElement(); + // 在所有的接口下再遍历IP + for (Enumeration inetAddresses = anInterface.getInetAddresses(); inetAddresses.hasMoreElements();) { + InetAddress inetAddr = inetAddresses.nextElement(); + // 排除loopback类型地址 + if (!inetAddr.isLoopbackAddress()) { + if (inetAddr.isSiteLocalAddress()) { + // 如果是site-local地址,就是它了 + return inetAddr.getHostAddress(); + } else if (candidateAddress == null) { + // site-local类型的地址未被发现,先记录候选地址 + candidateAddress = inetAddr; + } + } + } + } + if (candidateAddress != null) { + return candidateAddress.getHostAddress(); + } + // 如果没有发现 non-loopback地址.只能用最次选的方案 + InetAddress jdkSuppliedAddress = InetAddress.getLocalHost(); + if (jdkSuppliedAddress == null) { + return ""; + } + return jdkSuppliedAddress.getHostAddress(); + } catch (Exception e) { + return ""; + } + } +} diff --git a/backend/platform-system/pom.xml b/backend/platform-system/pom.xml new file mode 100644 index 0000000..7befed1 --- /dev/null +++ b/backend/platform-system/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + + com.yfd + platform + 1.0 + ../pom.xml + + + platform-system + 1.0 + jar + + Platform System + Platform System Module + + + + + com.yfd + platform-common + 1.0 + + + + + org.springframework.boot + spring-boot-starter + + + + + com.baomidou + mybatis-plus-spring-boot4-starter + + + + + org.springframework.boot + spring-boot-starter-security + + + + + \ No newline at end of file diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/PlatformApplication.java b/backend/platform-system/src/main/java/com/yfd/platform/system/PlatformApplication.java new file mode 100644 index 0000000..d35a8da --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/PlatformApplication.java @@ -0,0 +1,46 @@ +package com.yfd.platform.system; + +import com.yfd.platform.common.utils.SpringContextHolder; +import com.yfd.platform.system.annotation.rest.AnonymousGetMapping; +import com.yfd.platform.system.datasource.DynamicDataSourceConfig; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.data.redis.autoconfigure.DataRedisAutoConfiguration; +import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration; +import org.springframework.boot.web.server.servlet.context.ServletComponentScan; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.web.bind.annotation.RestController; + +//@SpringBootApplication +@RestController +@EnableTransactionManagement +@ServletComponentScan("com.yfd.platform.config") +@MapperScan(basePackages = "com.yfd.platform.*.*.mapper") +@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class, DataRedisAutoConfiguration.class}) +@Import({DynamicDataSourceConfig.class}) +@EnableCaching +public class PlatformApplication { + + public static void main(String[] args) { + SpringApplication.run(PlatformApplication.class, args); + } + + @Bean + public SpringContextHolder springContextHolder() { + return new SpringContextHolder(); + } + + /** + * 访问首页提示 + * + * @return / + */ + @AnonymousGetMapping("/") + public String index() { + return "Backend service started successfully"; + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/ServletInitializer.java b/backend/platform-system/src/main/java/com/yfd/platform/system/ServletInitializer.java new file mode 100644 index 0000000..b8e23ec --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/ServletInitializer.java @@ -0,0 +1,14 @@ +package com.yfd.platform.system; + +import com.yfd.platform.env.PlatformApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +public class ServletInitializer extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(PlatformApplication.class); + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/annotation/AnonymousAccess.java b/backend/platform-system/src/main/java/com/yfd/platform/system/annotation/AnonymousAccess.java new file mode 100644 index 0000000..697ed99 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/annotation/AnonymousAccess.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.annotation; + +import java.lang.annotation.*; + +/** + * @author jacky + * 用于标记匿名访问方法 + */ +@Inherited +@Documented +@Target({ElementType.METHOD,ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface AnonymousAccess { + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/annotation/Log.java b/backend/platform-system/src/main/java/com/yfd/platform/system/annotation/Log.java new file mode 100644 index 0000000..0550f14 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/annotation/Log.java @@ -0,0 +1,20 @@ +package com.yfd.platform.system.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author TangWei + * @date 2018-11-24 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Log { + + String value() default ""; + + String module() default ""; +} + diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/annotation/rest/AnonymousGetMapping.java b/backend/platform-system/src/main/java/com/yfd/platform/system/annotation/rest/AnonymousGetMapping.java new file mode 100644 index 0000000..b97909b --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/annotation/rest/AnonymousGetMapping.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yfd.platform.system.annotation.rest; + +import com.yfd.platform.annotation.AnonymousAccess; +import org.springframework.core.annotation.AliasFor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +import java.lang.annotation.*; + +/** + * Annotation for mapping HTTP {@code GET} requests onto specific handler + * methods. + *

+ * 支持匿名访问 GetMapping + * + * @author liaojinlong + * @see RequestMapping + */ +@AnonymousAccess +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RequestMapping(method = RequestMethod.GET) +public @interface AnonymousGetMapping { + + /** + * Alias for {@link RequestMapping#name}. + */ + @AliasFor(annotation = RequestMapping.class) + String name() default ""; + + /** + * Alias for {@link RequestMapping#value}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] value() default {}; + + /** + * Alias for {@link RequestMapping#path}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] path() default {}; + + /** + * Alias for {@link RequestMapping#params}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] params() default {}; + + /** + * Alias for {@link RequestMapping#headers}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] headers() default {}; + + /** + * Alias for {@link RequestMapping#consumes}. + * + * @since 4.3.5 + */ + @AliasFor(annotation = RequestMapping.class) + String[] consumes() default {}; + + /** + * Alias for {@link RequestMapping#produces}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] produces() default {}; + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/aspect/LogAspect.java b/backend/platform-system/src/main/java/com/yfd/platform/system/aspect/LogAspect.java new file mode 100644 index 0000000..330f78b --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/aspect/LogAspect.java @@ -0,0 +1,93 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.aspect; + +import com.yfd.platform.system.domain.SysLog; +import com.yfd.platform.system.mapper.SysUserMapper; +import com.yfd.platform.system.service.ISysLogService; +import com.yfd.platform.system.service.IUserService; +import com.yfd.platform.utils.RequestHolder; +import com.yfd.platform.utils.SecurityUtils; +import com.yfd.platform.utils.StringUtils; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * @author + * @date 2018-11-24 + */ +@Component +@Aspect +@Slf4j +public class LogAspect { + + @Resource + private final ISysLogService sysLogService; + + @Resource + private IUserService userService; + + ThreadLocal currentTime = new ThreadLocal<>(); + + public LogAspect(ISysLogService sysLogService) { + this.sysLogService = sysLogService; + } + + /** + * 配置切入点 + */ + @Pointcut("@annotation(com.yfd.platform.annotation.Log)") + public void logPointcut() { + // 该方法无方法体,主要为了让同类中其他方法使用此切入点 + } + + /** + * 配置环绕通知,使用在方法logPointcut()上注册的切入点 + * + * @param joinPoint join point for advice + */ + @Around("logPointcut()") + public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { + Object result; + currentTime.set(System.currentTimeMillis()); + result = joinPoint.proceed(); + SysLog log = new SysLog("INFO"); + currentTime.remove(); + HttpServletRequest request = RequestHolder.getHttpServletRequest(); + Map nameInfo = userService.getNameInfo(); + String nickname = nameInfo.get("nickname"); + String username = nameInfo.get("username"); + sysLogService.save(nickname, username, StringUtils.getBrowser(request), + StringUtils.getIp(request), joinPoint, log); + return result; + } + + public String getUsername() { + try { + return SecurityUtils.getCurrentUsername(); + } catch (Exception e) { + return ""; + } + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/component/ServerSendEventServer.java b/backend/platform-system/src/main/java/com/yfd/platform/system/component/ServerSendEventServer.java new file mode 100644 index 0000000..3f77766 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/component/ServerSendEventServer.java @@ -0,0 +1,147 @@ +package com.yfd.platform.system.component; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +/** + * @author Huhailong + * SSE Server send Event 服务器推送服务 + */ +@Slf4j +public class ServerSendEventServer { + + /** + * 当前连接数 + */ + private static AtomicInteger count = new AtomicInteger(0); + + private static Map sseEmitterMap = + new ConcurrentHashMap<>(); + + public static SseEmitter connect(String userId) { + //设置超时时间,0表示不过期,默认是30秒,超过时间未完成会抛出异常 + SseEmitter sseEmitter = new SseEmitter(0L); + //SseEmitter sseEmitter = new SseEmitter(); + //注册回调 + sseEmitter.onCompletion(completionCallBack(userId)); + sseEmitter.onError(errorCallBack(userId)); + sseEmitter.onTimeout(timeOutCallBack(userId)); + sseEmitterMap.put(userId, sseEmitter); + //数量+1 + count.getAndIncrement(); + log.info("create new sse connect ,current user:{}", userId); + return sseEmitter; + } + + /** + * 给指定用户发消息 + */ + public static void sendMessage(String userId, String message) { + if (sseEmitterMap.containsKey(userId)) { + try { + sseEmitterMap.get(userId).send(message); + } catch (IOException e) { + log.error("user id:{}, send message error:{}", userId, + e.getMessage()); + e.printStackTrace(); + } + } + } + + /** + * 给所有用户发消息 + */ + public static void sendMessage(String message) { + if (sseEmitterMap != null && !sseEmitterMap.isEmpty()) { + sseEmitterMap.forEach((k, v) -> { + // 发送消息 + sendMessage(k, message); + + }); + } + } + + /** + * 想多人发送消息,组播 + */ + public static void groupSendMessage(String groupId, String message) { + if (sseEmitterMap != null && !sseEmitterMap.isEmpty()) { + sseEmitterMap.forEach((k, v) -> { + try { + if (k.startsWith(groupId)) { + v.send(message, MediaType.APPLICATION_JSON); + } + } catch (IOException e) { + log.error("user id:{}, send message error:{}", groupId, + message); + removeUser(k); + } + }); + } + } + + public static void batchSendMessage(String message) { + sseEmitterMap.forEach((k, v) -> { + try { + v.send(message, MediaType.APPLICATION_JSON); + } catch (IOException e) { + log.error("user id:{}, send message error:{}", k, + e.getMessage()); + removeUser(k); + } + }); + } + + /** + * 群发消息 + */ + public static void batchSendMessage(String message, Set userIds) { + userIds.forEach(userId -> sendMessage(userId, message)); + } + + public static void removeUser(String userId) { + sseEmitterMap.remove(userId); + //数量-1 + count.getAndDecrement(); + log.info("remove user id:{}", userId); + } + + public static List getIds() { + return new ArrayList<>(sseEmitterMap.keySet()); + } + + public static int getUserCount() { + return count.intValue(); + } + + private static Runnable completionCallBack(String userId) { + return () -> { + log.info("结束连接,{}", userId); + removeUser(userId); + }; + } + + private static Runnable timeOutCallBack(String userId) { + return () -> { + log.info("连接超时,{}", userId); + removeUser(userId); + }; + } + + private static Consumer errorCallBack(String userId) { + return throwable -> { + log.error("连接异常,{}", userId); + removeUser(userId); + }; + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/component/WebSocketServer.java b/backend/platform-system/src/main/java/com/yfd/platform/system/component/WebSocketServer.java new file mode 100644 index 0000000..2f25f40 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/component/WebSocketServer.java @@ -0,0 +1,106 @@ +package com.yfd.platform.system.component; + +import org.springframework.stereotype.Component; + +import jakarta.websocket.*; +import jakarta.websocket.server.PathParam; +import jakarta.websocket.server.ServerEndpoint; +import java.io.IOException; +import java.util.concurrent.CopyOnWriteArrayList; + +@ServerEndpoint("/websocket/{token}") +@Component +public class WebSocketServer { + private static int onlineCount=0;//在线人数 + private static CopyOnWriteArrayList webSocketSet=new CopyOnWriteArrayList();//在线用户集合 + private Session session;//与某个客户端的连接会话 + private String currentUser; + + @OnOpen + public void onOpen(@PathParam("token") String token, Session session){ + this.currentUser = token; + this.session=session; + webSocketSet.add(this);//加入set中 + addOnlineCount(); + System.out.println("有新连接加入!当前在线人数为"+getOnlineCount()); + allCurrentOnline(); + } + + @OnClose + public void onClose(){ + webSocketSet.remove(this); + subOnlineCount(); + System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount()); + allCurrentOnline(); + } + + @OnMessage + public void onMessage(String message, Session session){ + System.out.println("来自客户端的消息:"+message); + for (WebSocketServer item:webSocketSet){ + try { + item.sendMessage(message); + } catch (IOException e) { + e.printStackTrace(); + continue; + } + } + } + + @OnError + public void onError(Session session, Throwable throwable){ + System.out.println("发生错误!"); + throwable.printStackTrace(); + } + + public void sendMessage(String message) throws IOException { + this.session.getBasicRemote().sendText(message); + } + + /** + * 获取当前所有在线用户名 + */ + public static void allCurrentOnline(){ + for (WebSocketServer item : webSocketSet) { + System.out.println(item.currentUser); + } + } + + /** + * 发送给指定用户 + */ + public static void sendMessageTo(String message,String token) throws IOException { + for (WebSocketServer item : webSocketSet) { + if(item.currentUser.equals(token)){ + item.session.getBasicRemote().sendText(message); + } + } + } + + /** + * 群发自定义消息 + */ + public static void sendInfo(String message) throws IOException { + System.out.println(message); + for (WebSocketServer item : webSocketSet) { + try { + item.sendMessage(message); + } catch (IOException e) { + continue; + } + } + } + + public static synchronized int getOnlineCount(){ + return onlineCount; + } + public static synchronized void addOnlineCount(){ + WebSocketServer.onlineCount++; + } + public static synchronized void subOnlineCount(){ + WebSocketServer.onlineCount--; + } + +} + + diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/config/AppInitProperties.java b/backend/platform-system/src/main/java/com/yfd/platform/system/config/AppInitProperties.java new file mode 100644 index 0000000..6c2af26 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/config/AppInitProperties.java @@ -0,0 +1,17 @@ +package com.yfd.platform.system.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "app.init") +public class AppInitProperties { + private boolean enabled = false; + private String schema; + private String data; + // 用于判断是否已初始化:默认检查是否存在核心表 + private String markerTable = "sys_user"; + private String markerVersion = "v1.0.0"; +} \ No newline at end of file diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/config/DataInitializer.java b/backend/platform-system/src/main/java/com/yfd/platform/system/config/DataInitializer.java new file mode 100644 index 0000000..24f279d --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/config/DataInitializer.java @@ -0,0 +1,129 @@ +package com.yfd.platform.system.config; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.jdbc.datasource.init.ScriptUtils; +import org.springframework.jdbc.datasource.init.ScriptException; +import org.springframework.jdbc.datasource.init.ScriptStatementFailedException; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.nio.charset.StandardCharsets; +import java.util.stream.Collectors; +import org.springframework.util.StreamUtils; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; + +@Slf4j +@Component +@Profile({"dev","server"}) +@RequiredArgsConstructor +@Order(Ordered.HIGHEST_PRECEDENCE) +public class DataInitializer implements ApplicationRunner { + + private final DataSource dataSource; + private final AppInitProperties properties; + private final ResourceLoader resourceLoader; + + @Override + public void run(ApplicationArguments args) throws Exception { + if (!properties.isEnabled()) { + log.info("[DataInit] 自动初始化已关闭 app.init.enabled=false"); + return; + } + + try (Connection conn = dataSource.getConnection()) { + boolean initialized = tableExists(conn, properties.getMarkerTable()); + if (initialized) { + log.info("[DataInit] 检测到标记表已存在: {},跳过初始化", properties.getMarkerTable()); + return; + } + + log.info("[DataInit] 未检测到标记表: {},开始导入 schema 与 data", properties.getMarkerTable()); + executeIfPresent(conn, properties.getSchema()); + executeIfPresent(conn, properties.getData()); + log.info("[DataInit] 导入完成。marker={} version={}", properties.getMarkerTable(), properties.getMarkerVersion()); + } catch (Exception ex) { + log.error("[DataInit] 初始化失败: {}", ex.getMessage(), ex); + throw ex; + } + } + + private boolean tableExists(Connection conn, String tableName) { + if (tableName == null || tableName.isEmpty()) return false; + try { + DatabaseMetaData meta = conn.getMetaData(); + try (ResultSet rs = meta.getTables(conn.getCatalog(), null, tableName, null)) { + return rs.next(); + } + } catch (Exception e) { + log.warn("[DataInit] 检查表存在异常: {}", e.getMessage()); + return false; + } + } + + private void executeIfPresent(Connection conn, String location) throws Exception { + if (location == null || location.isEmpty()) return; + Resource resource = resourceLoader.getResource(location); + if (!resource.exists()) { + log.warn("[DataInit] 资源不存在: {}", location); + return; + } + log.info("[DataInit] 执行脚本: {}", location); + // 读取并清理脚本首部的 BOM 和危险语句(CREATE DATABASE / USE schema) + String sql = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8); + if (sql != null && !sql.isEmpty()) { + // 移除 UTF-8 BOM(\uFEFF) + if (sql.charAt(0) == '\uFEFF') { + sql = sql.substring(1); + } + // 移除 "USE xxx;" 和 "CREATE DATABASE" 等与连接无关的语句 + sql = sql.lines() + .filter(line -> { + String raw = line; + String t = raw.trim(); + String u = t.toUpperCase(); + // 过滤与连接无关或可能引发解析问题的语句/注释 + if (u.startsWith("USE ")) return false; + if (u.startsWith("CREATE DATABASE")) return false; + if (t.startsWith("--")) return false; // 单行注释 + if (t.startsWith("/*") || t.startsWith("*/")) return false; // 多行注释行 + if (t.startsWith("/*!")) return false; // MySQL 版本注释 + if (t.matches("^-+$")) return false; // 分隔线 + return true; + }) + .collect(Collectors.joining("\n")); + } + if (sql != null && !sql.trim().isEmpty()) { + try { + ScriptUtils.executeSqlScript(conn, new EncodedResource(new ByteArrayResource(sql.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8)); + } catch (ScriptStatementFailedException e) { + String preview = sql.lines() + .filter(s -> !s.trim().isEmpty()) + .limit(10) + .collect(Collectors.joining("\n")); + log.error("[DataInit] SQL语句执行失败: {}\n前10行预览:\n{}", e.getMessage(), preview); + throw e; + } catch (ScriptException e) { + String preview = sql.lines() + .filter(s -> !s.trim().isEmpty()) + .limit(10) + .collect(Collectors.joining("\n")); + log.error("[DataInit] 脚本执行失败: {}\n前10行预览:\n{}", e.getMessage(), preview); + throw e; + } + } else { + log.warn("[DataInit] 脚本在清理后为空,跳过执行: {}", location); + } + } +} \ No newline at end of file diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/config/FileProperties.java b/backend/platform-system/src/main/java/com/yfd/platform/system/config/FileProperties.java new file mode 100644 index 0000000..5e56412 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/config/FileProperties.java @@ -0,0 +1,59 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * @author + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "file") +public class FileProperties { + + /** 文件大小限制 */ + private Long maxSize; + + /** 头像大小限制 */ + private Long avatarMaxSize; + + private ElPath mac; + + private ElPath linux; + + private ElPath windows; + + public ElPath getPath(){ + String os = System.getProperty("os.name"); + if(os.toLowerCase().startsWith("win")) { + return windows; + } else if(os.toLowerCase().startsWith("mac")){ + return mac; + } + return linux; + } + + @Data + public static class ElPath{ + + private String path; + + private String avatar; + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/config/FileSpaceProperties.java b/backend/platform-system/src/main/java/com/yfd/platform/system/config/FileSpaceProperties.java new file mode 100644 index 0000000..1b58d6f --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/config/FileSpaceProperties.java @@ -0,0 +1,17 @@ +package com.yfd.platform.system.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * 文件空间相关配置,替换 @Value("${file-space.system}") 用法 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "file-space") +public class FileSpaceProperties { + + /** 基础目录,例如 D:/data/platform/ */ + private String system; +} \ No newline at end of file diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/config/GlobalExceptionHandler.java b/backend/platform-system/src/main/java/com/yfd/platform/system/config/GlobalExceptionHandler.java new file mode 100644 index 0000000..b4bc8c5 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/config/GlobalExceptionHandler.java @@ -0,0 +1,25 @@ +package com.yfd.platform.system.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * @author TangWei + * @Date: 2023/3/27 18:07 + * @Description: + */ +@Slf4j +@ControllerAdvice +public class GlobalExceptionHandler { + + @ResponseBody + @ExceptionHandler(value = Throwable.class) + public ResponseResult handleException(Throwable e) { + log.error("message:{}", e.getMessage()); + e.printStackTrace(); + return ResponseResult.error(e.getMessage()); + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/config/JobRunner.java b/backend/platform-system/src/main/java/com/yfd/platform/system/config/JobRunner.java new file mode 100644 index 0000000..b394da0 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/config/JobRunner.java @@ -0,0 +1,59 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.config; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.yfd.platform.system.domain.QuartzJob; +import com.yfd.platform.system.mapper.QuartzJobMapper; +import com.yfd.platform.utils.QuartzManage; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * @author + * @date 2019-01-07 + */ +@Component +@RequiredArgsConstructor +@Order(Ordered.LOWEST_PRECEDENCE) +public class JobRunner implements ApplicationRunner { + + private static final Logger log = LoggerFactory.getLogger(JobRunner.class); + private final QuartzJobMapper quartzJobMapper; + private final QuartzManage quartzManage; + + /** + * 项目启动时重新激活启用的定时任务 + * + * @param applicationArguments / + */ + @Override + public void run(ApplicationArguments applicationArguments) { + log.info("--------------------注入定时任务---------------------"); + List quartzJobs = + quartzJobMapper.selectList(new LambdaQueryWrapper().eq(QuartzJob::getStatus, "1")); + quartzJobs.forEach(quartzManage::addJob); + log.info("--------------------定时任务注入完成---------------------"); + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/config/JwtAuthenticationTokenFilter.java b/backend/platform-system/src/main/java/com/yfd/platform/system/config/JwtAuthenticationTokenFilter.java new file mode 100644 index 0000000..396f9e7 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/config/JwtAuthenticationTokenFilter.java @@ -0,0 +1,83 @@ +package com.yfd.platform.system.config; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.yfd.platform.component.ServerSendEventServer; +import com.yfd.platform.constant.Constant; +import com.yfd.platform.system.domain.LoginUser; +import com.yfd.platform.system.domain.Message; +import com.yfd.platform.system.service.IMessageService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.annotation.Resource; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Component +public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { + + @Autowired + private WebConfig webConfig; + + @Override + protected void doFilterInternal(HttpServletRequest httpServletRequest, + HttpServletResponse httpServletResponse, + FilterChain filterChain) throws ServletException, IOException { + //获取token + String uri = httpServletRequest.getRequestURI(); + String token = httpServletRequest.getHeader("token"); + if (StrUtil.isEmpty(token) || "/user/login".equals(uri)) { + filterChain.doFilter(httpServletRequest, httpServletResponse); + return; + } + //解析token + boolean isok = JWTUtil.verify(token, "12345678".getBytes()); + String userid = ""; + if (isok) { + final JWT jwt = JWTUtil.parseToken(token); + userid = jwt.getPayload("userid").toString(); + //从cachekey中获取用户信息失效时间 + String cachekey = "expire_time:" + userid; + if(StrUtil.isNotEmpty(webConfig.loginuserCache().get(cachekey))){ + long expire_time =Long.parseLong(webConfig.loginuserCache().get(cachekey)); + if (System.currentTimeMillis() > expire_time) { + httpServletResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Token超过期限!"); + return; + } + } + } + + //从cachekey中获取用户信息 + String cachekey = "login:" + userid; + String jsonstr = webConfig.loginuserCache().get(cachekey); + LoginUser loginUser = JSON.parseObject(jsonstr, LoginUser.class); + if (ObjectUtil.isEmpty(loginUser)) { + httpServletResponse.sendError(HttpServletResponse.SC_FORBIDDEN, + "登录用户已失效!"); + return; + } + //存入SecurityContextHolder + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(loginUser, null, + loginUser.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + webConfig.loginuserCache().put(Constant.TOKEN + userid, token); + //更新了超期时间 + long expireTime =System.currentTimeMillis() + ( 30L * 60L * 1000L); + webConfig.loginuserCache().put("expire_time:" + userid, String.valueOf(expireTime)); + //放行过滤器 + filterChain.doFilter(httpServletRequest, httpServletResponse); + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/config/MessageConfig.java b/backend/platform-system/src/main/java/com/yfd/platform/system/config/MessageConfig.java new file mode 100644 index 0000000..9535d15 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/config/MessageConfig.java @@ -0,0 +1,50 @@ +package com.yfd.platform.system.config; + +import cn.hutool.cache.Cache; +import cn.hutool.cache.impl.CacheObj; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.yfd.platform.component.ServerSendEventServer; +import com.yfd.platform.constant.Constant; +import com.yfd.platform.system.domain.Message; +import com.yfd.platform.system.domain.SysUser; +import com.yfd.platform.system.service.IMessageService; +import com.yfd.platform.system.service.IUserService; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import java.util.Iterator; + +/** + * @author TangWei + * @Date: 2023/3/24 15:56 + * @Description: + */ +@Component +public class MessageConfig { + + @Resource + private IMessageService messageService; + + @Resource + private IUserService userService; + + @Resource + private WebConfig webConfig; + + public void sendMessage() { + long count = + messageService.count(new LambdaQueryWrapper().eq(Message::getStatus, "1")); + String userId = userService.getUserInfo().getId(); + String token = webConfig.loginuserCache().get(Constant.TOKEN + userId); + ServerSendEventServer.sendMessage(token, count + ""); + } + + public void addMessage(Message message) { + messageService.save(message); + long count = + messageService.count(new LambdaQueryWrapper().eq(Message::getStatus, "1")); + ServerSendEventServer.sendMessage(count + ""); + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/config/MybitsPlusConfig.java b/backend/platform-system/src/main/java/com/yfd/platform/system/config/MybitsPlusConfig.java new file mode 100644 index 0000000..8ed050a --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/config/MybitsPlusConfig.java @@ -0,0 +1,35 @@ +package com.yfd.platform.system.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/****************************** + * 用途说明: + * 作者姓名: pcj + * 创建时间: 2022/10/24 10:50 + ******************************/ +@Configuration +public class MybitsPlusConfig { + +// @Bean +// public MybatisPlusInterceptor mybatisPlusInterceptor() { +// MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); +// mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); +// return mybatisPlusInterceptor; +// } + + /** + * 分页插件配置(Oracle 兼容) + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + // 添加分页拦截器,指定数据库类型为 Oracle + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.ORACLE)); + return interceptor; + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/config/ProdApiPrefixFilter.java b/backend/platform-system/src/main/java/com/yfd/platform/system/config/ProdApiPrefixFilter.java new file mode 100644 index 0000000..75aad0e --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/config/ProdApiPrefixFilter.java @@ -0,0 +1,44 @@ +package com.yfd.platform.system.config; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.annotation.WebFilter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 将以 /prod-api/ 开头的请求转发到去掉前缀的真实后端接口路径。 + * 例如:/prod-api/user/code -> /user/code + * 这样可以兼容前端生产环境仍使用 /prod-api 作为网关前缀的情况。 + */ +@WebFilter(urlPatterns = "/prod-api/*", filterName = "prodApiPrefixFilter") +public class ProdApiPrefixFilter implements Filter { + + private static final String PREFIX = "/prod-api"; + + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { + if (!(req instanceof HttpServletRequest) || !(res instanceof HttpServletResponse)) { + chain.doFilter(req, res); + return; + } + + HttpServletRequest request = (HttpServletRequest) req; + String uri = request.getRequestURI(); + + // 仅拦截 /prod-api/* 的接口请求并进行内部 forward + if (uri.startsWith(PREFIX + "/")) { + String forwardUri = uri.substring(PREFIX.length()); + RequestDispatcher dispatcher = request.getRequestDispatcher(forwardUri); + dispatcher.forward(req, res); + return; + } + + chain.doFilter(req, res); + } +} \ No newline at end of file diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/config/QuartzConfig.java b/backend/platform-system/src/main/java/com/yfd/platform/system/config/QuartzConfig.java new file mode 100644 index 0000000..4ccad1c --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/config/QuartzConfig.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.config; + +import org.quartz.Scheduler; +import org.quartz.spi.TriggerFiredBundle; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.quartz.AdaptableJobFactory; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +/** + * 定时任务配置 + * + * @author / + * @date 2019-01-07 + */ +@Configuration +public class QuartzConfig { + + /** + * 解决Job中注入Spring Bean为null的问题 + */ + @Component("quartzJobFactory") + public static class QuartzJobFactory extends AdaptableJobFactory { + + private final AutowireCapableBeanFactory capableBeanFactory; + + public QuartzJobFactory(AutowireCapableBeanFactory capableBeanFactory) { + this.capableBeanFactory = capableBeanFactory; + } + + @Override + protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { + + //调用父类的方法 + Object jobInstance = super.createJobInstance(bundle); + capableBeanFactory.autowireBean(jobInstance); + return jobInstance; + } + } + + /** + * 注入scheduler到spring + * + * @param quartzJobFactory / + * @return Scheduler + * @throws Exception / + */ + @Bean(name = "scheduler") + public Scheduler scheduler(QuartzJobFactory quartzJobFactory) throws Exception { + SchedulerFactoryBean factoryBean = new SchedulerFactoryBean(); + factoryBean.setJobFactory(quartzJobFactory); + factoryBean.afterPropertiesSet(); + Scheduler scheduler = factoryBean.getScheduler(); + scheduler.start(); + return scheduler; + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/config/ResponseResult.java b/backend/platform-system/src/main/java/com/yfd/platform/system/config/ResponseResult.java new file mode 100644 index 0000000..a4a5c36 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/config/ResponseResult.java @@ -0,0 +1,57 @@ +package com.yfd.platform.system.config; + +import java.util.HashMap; + +public class ResponseResult extends HashMap { + private static final long serialVersionUID = 1L; + + public ResponseResult() { + } + + public static ResponseResult unlogin() { + return message("401", "未登录"); + } + + public static ResponseResult error() { + return error("操作失败"); + } + + public static ResponseResult success() { + return success("操作成功"); + } + + public static ResponseResult error(String msg) { + ResponseResult json = new ResponseResult(); + json.put((String)"code", "1");//错误 + json.put((String)"msg", msg); + return json; + } + + public static ResponseResult message(String code, String msg) { + ResponseResult json = new ResponseResult(); + json.put((String)"code", code); + json.put((String)"msg", msg); + return json; + } + + public static ResponseResult success(String msg) { + ResponseResult json = new ResponseResult(); + json.put((String)"code", "0");//正常 + json.put((String)"msg", msg); + return json; + } + + public static ResponseResult successData(Object obj) { + ResponseResult json = new ResponseResult(); + json.put((String)"code", "0");//正常 + json.put((String)"msg", "操作成功"); + json.put("data", obj); + return json; + } + + + public ResponseResult put(String key, Object value) { + super.put(key, value); + return this; + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/config/SecurityConfig.java b/backend/platform-system/src/main/java/com/yfd/platform/system/config/SecurityConfig.java new file mode 100644 index 0000000..abd6062 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/config/SecurityConfig.java @@ -0,0 +1,90 @@ +package com.yfd.platform.system.config; + +import com.yfd.platform.config.bean.LoginProperties; +import com.yfd.platform.exception.AccessDeniedHandExcetion; +import com.yfd.platform.exception.AuthenticationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +public class SecurityConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + @ConfigurationProperties(prefix = "login", ignoreUnknownFields = true) + public LoginProperties loginProperties() { + return new LoginProperties(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } + + @Autowired + private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; + + @Autowired + private AuthenticationException authenticationException; + + @Autowired + private AccessDeniedHandExcetion accessDeniedHandExcetion; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/user/login").anonymous() + .requestMatchers("/user/code").permitAll() + .requestMatchers(HttpMethod.GET, "/").permitAll() + .requestMatchers(HttpMethod.GET, + "/*.html", + "/webSocket/**", + "/assets/**", + "/icon/**").permitAll() + .requestMatchers( + "/swagger-ui.html", + "/swagger-ui/**", + "/v3/api-docs/**", + "/v3/api-docs.yaml", + "/swagger-resources/**", + "/webjars/**", + "/*/api-docs").permitAll() + .requestMatchers( + "/report/**", + "/images/**", + "/pageimage/**", + "/avatar/**", + "/systemurl/**", + "/api/imageserver/upload").permitAll() + .anyRequest().authenticated() + ) + .cors(cors -> {}); + + http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); + + http.exceptionHandling(ex -> ex + .authenticationEntryPoint(authenticationException) + .accessDeniedHandler(accessDeniedHandExcetion) + ); + + return http.build(); + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/config/SwaggerConfig.java b/backend/platform-system/src/main/java/com/yfd/platform/system/config/SwaggerConfig.java new file mode 100644 index 0000000..683b4c2 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/config/SwaggerConfig.java @@ -0,0 +1,49 @@ +package com.yfd.platform.system.config; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springdoc.core.models.GroupedOpenApi; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.Contact; + +/** + * Springdoc OpenAPI 配置 + */ +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI projectOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("项目API 接口文档") + .version("3.0") + .description("") + .contact(new Contact().name("郑顺利").email("13910913995@163.com")) + ); + } + + @Bean + public GroupedOpenApi groupWebsiteApi() { + return GroupedOpenApi.builder() + .group("1. 平台模块") + .packagesToScan("com.yfd.platform.modules.platformdb.controller") + .build(); + } + + @Bean + public GroupedOpenApi groupQuartzApi() { + return GroupedOpenApi.builder() + .group("2. 定时任务") + .packagesToScan("com.yfd.platform.modules.quartz.controller") + .build(); + } + + @Bean + public GroupedOpenApi groupSystemApi() { + return GroupedOpenApi.builder() + .group("3. 系统管理") + .packagesToScan("com.yfd.platform.system.controller") + .build(); + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/config/WebConfig.java b/backend/platform-system/src/main/java/com/yfd/platform/system/config/WebConfig.java new file mode 100644 index 0000000..f9b5dd6 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/config/WebConfig.java @@ -0,0 +1,61 @@ +package com.yfd.platform.system.config; + +import cn.hutool.cache.Cache; +import cn.hutool.cache.CacheUtil; +import lombok.SneakyThrows; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + @Resource + private FileSpaceProperties fileSpaceProperties; + + + + @Bean + public Cache loginuserCache() { + return CacheUtil.newLRUCache(200);//用户登录缓存数 缺省200 + } + + @Bean + public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = + new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOriginPattern("*"); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + config.setMaxAge(3600L); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } + + @SneakyThrows + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/icon/**") + .addResourceLocations("classpath:/static/icon/") + .setCachePeriod(0); + + registry.addResourceHandler("/assets/**") + .addResourceLocations("classpath:/static/assets/") + .setCachePeriod(0); + + registry.addResourceHandler("swagger-ui.html").addResourceLocations( + "classpath:/META-INF/resources/"); + + String systemUrl = "file:" + fileSpaceProperties.getSystem().replace("\\", "/")+"user\\"; + registry.addResourceHandler("/avatar/**").addResourceLocations(systemUrl).setCachePeriod(0); + + + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/config/WebSocketConfig.java b/backend/platform-system/src/main/java/com/yfd/platform/system/config/WebSocketConfig.java new file mode 100644 index 0000000..50b91b9 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/config/WebSocketConfig.java @@ -0,0 +1,16 @@ +package com.yfd.platform.system.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.server.standard.ServerEndpointExporter; + +@Configuration +public class WebSocketConfig { + + @Bean + public ServerEndpointExporter serverEndpointExporter() { + + return new ServerEndpointExporter(); + } +} + diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/config/bean/LoginCode.java b/backend/platform-system/src/main/java/com/yfd/platform/system/config/bean/LoginCode.java new file mode 100644 index 0000000..3ab9cbe --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/config/bean/LoginCode.java @@ -0,0 +1,61 @@ +/* + * Copyright 2019-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.config.bean; + +import lombok.Data; + +/** + * 登录验证码配置信息 + * + * @author: liaojinlong + * @date: 2020/6/10 18:53 + */ +@Data +public class LoginCode { + + /** + * 验证码配置 + */ + private LoginCodeEnum codeType; + /** + * 验证码有效期 分钟 + */ + private Long expiration = 2L; + /** + * 验证码内容长度 + */ + private int length = 2; + /** + * 验证码宽度 + */ + private int width = 111; + /** + * 验证码高度 + */ + private int height = 36; + /** + * 验证码字体 + */ + private String fontName; + /** + * 字体大小 + */ + private int fontSize = 25; + + public LoginCodeEnum getCodeType() { + return codeType; + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/config/bean/LoginCodeEnum.java b/backend/platform-system/src/main/java/com/yfd/platform/system/config/bean/LoginCodeEnum.java new file mode 100644 index 0000000..f02266c --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/config/bean/LoginCodeEnum.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.config.bean; + +/** + * 验证码配置枚举 + * + * @author: liaojinlong + * @date: 2020/6/10 17:40 + */ + +public enum LoginCodeEnum { + /** + * 算数 + */ + arithmetic, + /** + * 中文 + */ + chinese, + /** + * 中文闪图 + */ + chinese_gif, + /** + * 闪图 + */ + gif, + spec +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/config/bean/LoginProperties.java b/backend/platform-system/src/main/java/com/yfd/platform/system/config/bean/LoginProperties.java new file mode 100644 index 0000000..43a75e2 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/config/bean/LoginProperties.java @@ -0,0 +1,110 @@ +/* + * Copyright 2019-2020 the original author or authors. + * + * Licensed under the Apache License, Version loginCode.length.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-loginCode.length.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.config.bean; + +import cn.hutool.core.util.StrUtil; +import com.wf.captcha.*; +import com.wf.captcha.base.Captcha; +import com.yfd.platform.exception.BadConfigurationException; +import lombok.Data; +import java.awt.*; +import java.util.Objects; + +/** + * 配置文件读取 + * + * @author liaojinlong + * @date loginCode.length0loginCode.length0/6/10 17:loginCode.length6 + */ +@Data +public class LoginProperties { + + /** + * 账号单用户 登录 + */ + private boolean singleLogin = false; + + private LoginCode loginCode; + /** + * 用户登录信息缓存 + */ + private boolean cacheEnable; + + public boolean isSingleLogin() { + return singleLogin; + } + + public boolean isCacheEnable() { + return cacheEnable; + } + + /** + * 获取验证码生产类 + * + * @return / + */ + public Captcha getCaptcha() { + if (Objects.isNull(loginCode)) { + loginCode = new LoginCode(); + if (Objects.isNull(loginCode.getCodeType())) { + loginCode.setCodeType(LoginCodeEnum.arithmetic); + } + } + return switchCaptcha(loginCode); + } + + /** + * 依据配置信息生产验证码 + * + * @param loginCode 验证码配置信息 + * @return / + */ + private Captcha switchCaptcha(LoginCode loginCode) { + Captcha captcha; + synchronized (this) { + switch (loginCode.getCodeType()) { + case arithmetic: + // 算术类型 https://gitee.com/whvse/EasyCaptcha + captcha = new ArithmeticCaptcha(loginCode.getWidth(), loginCode.getHeight()); + // 几位数运算,默认是两位 + captcha.setLen(loginCode.getLength()); + break; + case chinese: + captcha = new ChineseCaptcha(loginCode.getWidth(), loginCode.getHeight()); + captcha.setLen(loginCode.getLength()); + break; + case chinese_gif: + captcha = new ChineseGifCaptcha(loginCode.getWidth(), loginCode.getHeight()); + captcha.setLen(loginCode.getLength()); + break; + case gif: + captcha = new GifCaptcha(loginCode.getWidth(), loginCode.getHeight()); + captcha.setLen(loginCode.getLength()); + break; + case spec: + captcha = new SpecCaptcha(loginCode.getWidth(), loginCode.getHeight()); + captcha.setLen(loginCode.getLength()); + break; + default: + throw new BadConfigurationException("验证码配置信息错误!正确配置查看 LoginCodeEnum "); + } + } + if(StrUtil.isNotBlank(loginCode.getFontName())){ + captcha.setFont(new Font(loginCode.getFontName(), Font.PLAIN, loginCode.getFontSize())); + } + return captcha; + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/config/thread/AsyncTaskExecutePool.java b/backend/platform-system/src/main/java/com/yfd/platform/system/config/thread/AsyncTaskExecutePool.java new file mode 100644 index 0000000..afad8a3 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/config/thread/AsyncTaskExecutePool.java @@ -0,0 +1,70 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.config.thread; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 异步任务线程池装配类 + * @author https://juejin.im/entry/5abb8f6951882555677e9da2 + * @date 2019年10月31日15:06:18 + */ +@Slf4j +@Configuration +public class AsyncTaskExecutePool implements AsyncConfigurer { + + /** 注入配置类 */ + private final AsyncTaskProperties config; + + public AsyncTaskExecutePool(AsyncTaskProperties config) { + this.config = config; + } + + @Override + public Executor getAsyncExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + //核心线程池大小 + executor.setCorePoolSize(config.getCorePoolSize()); + //最大线程数 + executor.setMaxPoolSize(config.getMaxPoolSize()); + //队列容量 + executor.setQueueCapacity(config.getQueueCapacity()); + //活跃时间 + executor.setKeepAliveSeconds(config.getKeepAliveSeconds()); + //线程名字前缀 + executor.setThreadNamePrefix("el-async-"); + // setRejectedExecutionHandler:当pool已经达到max size的时候,如何处理新任务 + // CallerRunsPolicy:不在新线程中执行任务,而是由调用者所在的线程来执行 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + executor.initialize(); + return executor; + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return (throwable, method, objects) -> { + log.error("===="+throwable.getMessage()+"====", throwable); + log.error("exception method:"+method.getName()); + }; + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/config/thread/AsyncTaskProperties.java b/backend/platform-system/src/main/java/com/yfd/platform/system/config/thread/AsyncTaskProperties.java new file mode 100644 index 0000000..35adb9e --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/config/thread/AsyncTaskProperties.java @@ -0,0 +1,39 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.config.thread; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 线程池配置属性类 + * @author https://juejin.im/entry/5abb8f6951882555677e9da2 + * @date 2019年10月31日14:58:18 + */ +@Data +@Component +@ConfigurationProperties(prefix = "task.pool") +public class AsyncTaskProperties { + + private int corePoolSize; + + private int maxPoolSize; + + private int keepAliveSeconds; + + private int queueCapacity; +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/config/thread/TheadFactoryName.java b/backend/platform-system/src/main/java/com/yfd/platform/system/config/thread/TheadFactoryName.java new file mode 100644 index 0000000..6ac93f2 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/config/thread/TheadFactoryName.java @@ -0,0 +1,62 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.config.thread; + +import org.springframework.stereotype.Component; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 自定义线程名称 + * @author + * @date 2019年10月31日17:49:55 + */ +@Component +public class TheadFactoryName implements ThreadFactory { + + private static final AtomicInteger POOL_NUMBER = new AtomicInteger(1); + private final ThreadGroup group; + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + + public TheadFactoryName() { + this("el-pool"); + } + + private TheadFactoryName(String name){ + // 使用当前线程的线程组,避免依赖已弃用的 SecurityManager + group = Thread.currentThread().getThreadGroup(); + //此时namePrefix就是 name + 第几个用这个工厂创建线程池的 + this.namePrefix = name + + POOL_NUMBER.getAndIncrement(); + } + + @Override + public Thread newThread(Runnable r) { + //此时线程的名字 就是 namePrefix + -thread- + 这个线程池中第几个执行的线程 + Thread t = new Thread(group, r, + namePrefix + "-thread-"+threadNumber.getAndIncrement(), + 0); + if (t.isDaemon()) { + t.setDaemon(false); + } + if (t.getPriority() != Thread.NORM_PRIORITY) { + t.setPriority(Thread.NORM_PRIORITY); + } + return t; + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/config/thread/ThreadPoolExecutorUtil.java b/backend/platform-system/src/main/java/com/yfd/platform/system/config/thread/ThreadPoolExecutorUtil.java new file mode 100644 index 0000000..d4224be --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/config/thread/ThreadPoolExecutorUtil.java @@ -0,0 +1,44 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.config.thread; + + + +import com.yfd.platform.utils.SpringContextHolder; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * 用于获取自定义线程池 + * @author + * @date 2019年10月31日18:16:47 + */ +public class ThreadPoolExecutorUtil { + + public static ThreadPoolExecutor getPoll(){ + AsyncTaskProperties properties = SpringContextHolder.getBean(AsyncTaskProperties.class); + return new ThreadPoolExecutor( + properties.getCorePoolSize(), + properties.getMaxPoolSize(), + properties.getKeepAliveSeconds(), + TimeUnit.SECONDS, + new ArrayBlockingQueue<>(properties.getQueueCapacity()), + new TheadFactoryName() + ); + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/constant/Constant.java b/backend/platform-system/src/main/java/com/yfd/platform/system/constant/Constant.java new file mode 100644 index 0000000..23a1044 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/constant/Constant.java @@ -0,0 +1,42 @@ +package com.yfd.platform.system.constant; + +/** + * @author TangWei + * @Date: 2023/3/3 17:40 + * @Description: 常量类 + */ +public class Constant { + + public static final String LOGIN = "login:"; + public static final String TOKEN = "token:"; + public static final String USER_ID = "userid"; + public static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + public static final String CODE_KEY = "code-key-"; + public static final long CODE_EXPIRATION_TIME = 1000 * 60; + /** + * 用于IP定位转换 + */ + public static final String REGION = "内网IP|内网IP"; + /** + * win 系统 + */ + public static final String WIN = "win"; + + /** + * mac 系统 + */ + public static final String MAC = "mac"; + + /** + * 常用接口 + */ + public static class Url { + + // IP归属地查询 + // public static final String IP_URL = "http://whois.pconline.com + // .cn/ipJson.jsp?ip=%s&json=true"; + public static final String IP_URL = "http://whois.pconline.com" + + ".cn/ipJson.jsp?ip=%s&json=true"; + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/datasource/DataSource.java b/backend/platform-system/src/main/java/com/yfd/platform/system/datasource/DataSource.java new file mode 100644 index 0000000..a6cf3ff --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/datasource/DataSource.java @@ -0,0 +1,17 @@ +package com.yfd.platform.system.datasource; + +import java.lang.annotation.*; + +/****************************** + * 用途说明: + * 作者姓名: wxy + * 创建时间: 2022/9/23 17:48 + ******************************/ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DataSource { + + String name() default ""; + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/datasource/DataSourceAspect.java b/backend/platform-system/src/main/java/com/yfd/platform/system/datasource/DataSourceAspect.java new file mode 100644 index 0000000..56556e2 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/datasource/DataSourceAspect.java @@ -0,0 +1,55 @@ +package com.yfd.platform.system.datasource; + +import cn.hutool.core.util.StrUtil; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; + +/****************************** + * 用途说明: + * 作者姓名: wxy + * 创建时间: 2022/9/23 17:50 + ******************************/ +@Aspect +@Component +public class DataSourceAspect { + + @Pointcut("@annotation(com.yfd.platform.datasource.DataSource)") + public void dataSourcePointCut() { + + } + + private String DataBaseName; + + @Around("dataSourcePointCut()") + public Object around(ProceedingJoinPoint point) throws Throwable { + MethodSignature signature = (MethodSignature) point.getSignature(); + Method method = signature.getMethod(); + if (StrUtil.isNotBlank(DataBaseName)){ + DynamicDataSource.setDataSource(DataBaseName); + }else { + DynamicDataSource.setDataSource("master"); + } + + try { + return point.proceed(); + } finally { + DynamicDataSource.clearDataSource(); + } + } + + public String getDataBase(Integer type){ + if (type == 1){ + DataBaseName="master"; + }else { + DataBaseName="slave"; + } + return DataBaseName; + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/datasource/DynamicDataSource.java b/backend/platform-system/src/main/java/com/yfd/platform/system/datasource/DynamicDataSource.java new file mode 100644 index 0000000..7dfbf40 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/datasource/DynamicDataSource.java @@ -0,0 +1,40 @@ +package com.yfd.platform.system.datasource; + +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; + +import javax.sql.DataSource; +import java.util.Map; + +/****************************** + * 用途说明: + * 作者姓名: wxy + * 创建时间: 2022/9/23 17:47 + ******************************/ +public class DynamicDataSource extends AbstractRoutingDataSource { + private static final ThreadLocal contextHolder = new ThreadLocal<>(); + + public DynamicDataSource(DataSource defaultTargetDataSource, Map targetDataSources) { + super.setDefaultTargetDataSource(defaultTargetDataSource); + super.setTargetDataSources(targetDataSources); + super.afterPropertiesSet(); + } + + @Override + protected Object determineCurrentLookupKey() { + return getDataSource(); + } + + public static void setDataSource(String dataSource) { + contextHolder.set(dataSource); + } + + public static String getDataSource() { + return contextHolder.get(); + } + + public static void clearDataSource() { + contextHolder.remove(); + } + + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/datasource/DynamicDataSourceConfig.java b/backend/platform-system/src/main/java/com/yfd/platform/system/datasource/DynamicDataSourceConfig.java new file mode 100644 index 0000000..e54c563 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/datasource/DynamicDataSourceConfig.java @@ -0,0 +1,51 @@ +package com.yfd.platform.system.datasource; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; +import com.alibaba.druid.pool.DruidDataSource; + +import javax.sql.DataSource; +import java.util.HashMap; +import java.util.Map; + +/****************************** + * 用途说明: + * 作者姓名: wxy + * 创建时间: 2022/9/23 17:45 + ******************************/ +@Configuration +@Component +public class DynamicDataSourceConfig { + + @Bean + @ConfigurationProperties("spring.datasource.druid.master") + public DataSource wglMasterDataSource(){ + DruidDataSource dataSource = new DruidDataSource(); + dataSource.setBreakAfterAcquireFailure(true); + dataSource.setConnectionErrorRetryAttempts(0); + return dataSource; + } + + @Bean + @ConfigurationProperties("spring.datasource.druid.slave") + public DataSource wglSlaveDataSource(){ + DruidDataSource dataSource = new DruidDataSource(); + dataSource.setBreakAfterAcquireFailure(true); + dataSource.setConnectionErrorRetryAttempts(0); + return dataSource; + } + + @Bean + @Primary + public DynamicDataSource dataSource(DataSource wglMasterDataSource, DataSource wglSlaveDataSource) { + Map targetDataSources = new HashMap<>(); + targetDataSources.put("master",wglMasterDataSource); + targetDataSources.put("slave",wglSlaveDataSource); + return new DynamicDataSource(wglMasterDataSource, targetDataSources); + } + + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/exception/AccessDeniedHandExcetion.java b/backend/platform-system/src/main/java/com/yfd/platform/system/exception/AccessDeniedHandExcetion.java new file mode 100644 index 0000000..545417e --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/exception/AccessDeniedHandExcetion.java @@ -0,0 +1,27 @@ +package com.yfd.platform.system.exception; + +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +@Component +public class AccessDeniedHandExcetion implements AccessDeniedHandler { + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { + JSONObject jobj=new JSONObject(); + jobj.putOnce("status","403"); + jobj.putOnce("msg","用户权限不足,不能访问"); + response.setStatus(200); + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + response.getWriter().println(JSONUtil.toJsonStr(jobj)); + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/exception/AuthenticationException.java b/backend/platform-system/src/main/java/com/yfd/platform/system/exception/AuthenticationException.java new file mode 100644 index 0000000..738eb94 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/exception/AuthenticationException.java @@ -0,0 +1,29 @@ +package com.yfd.platform.system.exception; + +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; +@Component +public class AuthenticationException implements AuthenticationEntryPoint { + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, org.springframework.security.core.AuthenticationException authException) throws IOException, ServletException { + JSONObject jobj=new JSONObject(); + if(authException.getMessage().equals("用户账号不存在!")){ + jobj.putOnce("code","401"); + jobj.putOnce("msg","用户账号不存在/密码错误,登录失败!"); + }else{ + jobj.putOnce("code","401"); + jobj.putOnce("msg","用户Token失效,请重新登录!"); + } + response.setStatus(200); + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + response.getWriter().println(JSONUtil.toJsonStr(jobj)); + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/exception/BadConfigurationException.java b/backend/platform-system/src/main/java/com/yfd/platform/system/exception/BadConfigurationException.java new file mode 100644 index 0000000..e910baf --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/exception/BadConfigurationException.java @@ -0,0 +1,98 @@ +/* + * Copyright 2019-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.exception; + +/** + * 统一关于错误配置信息 异常 + * + * @author: liaojinlong + * @date: 2020/6/10 18:06 + */ +public class BadConfigurationException extends RuntimeException { + /** + * Constructs a new runtime exception with {@code null} as its + * detail message. The cause is not initialized, and may subsequently be + * initialized by a call to {@link #initCause}. + */ + public BadConfigurationException() { + super(); + } + + /** + * Constructs a new runtime exception with the specified detail message. + * The cause is not initialized, and may subsequently be initialized by a + * call to {@link #initCause}. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public BadConfigurationException(String message) { + super(message); + } + + /** + * Constructs a new runtime exception with the specified detail message and + * cause.

Note that the detail message associated with + * {@code cause} is not automatically incorporated in + * this runtime exception's detail message. + * + * @param message the detail message (which is saved for later retrieval + * by the {@link #getMessage()} method). + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A {@code null} value is + * permitted, and indicates that the cause is nonexistent or + * unknown.) + * @since 1.4 + */ + public BadConfigurationException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new runtime exception with the specified cause and a + * detail message of {@code (cause==null ? null : cause.toString())} + * (which typically contains the class and detail message of + * {@code cause}). This constructor is useful for runtime exceptions + * that are little more than wrappers for other throwables. + * + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A {@code null} value is + * permitted, and indicates that the cause is nonexistent or + * unknown.) + * @since 1.4 + */ + public BadConfigurationException(Throwable cause) { + super(cause); + } + + /** + * Constructs a new runtime exception with the specified detail + * message, cause, suppression enabled or disabled, and writable + * stack trace enabled or disabled. + * + * @param message the detail message. + * @param cause the cause. (A {@code null} value is permitted, + * and indicates that the cause is nonexistent or unknown.) + * @param enableSuppression whether or not suppression is enabled + * or disabled + * @param writableStackTrace whether or not the stack trace should + * be writable + * @since 1.7 + */ + protected BadConfigurationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/exception/BadRequestException.java b/backend/platform-system/src/main/java/com/yfd/platform/system/exception/BadRequestException.java new file mode 100644 index 0000000..8ab437b --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/exception/BadRequestException.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +/** + * @author + * @date 2018-11-23 + * 统一异常处理 + */ +@Getter +public class BadRequestException extends RuntimeException{ + + private Integer status = BAD_REQUEST.value(); + + public BadRequestException(String msg){ + super(msg); + } + + public BadRequestException(HttpStatus status, String msg){ + super(msg); + this.status = status.value(); + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/exception/ChildrenExistException.java b/backend/platform-system/src/main/java/com/yfd/platform/system/exception/ChildrenExistException.java new file mode 100644 index 0000000..64d8b04 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/exception/ChildrenExistException.java @@ -0,0 +1,20 @@ +package com.yfd.platform.system.exception; + +import org.springframework.util.StringUtils; + +/** + * @Author pcj + * @Date 2021/1/26 9:07 + * @Version 1.0 + */ +public class ChildrenExistException extends RuntimeException{ + + public ChildrenExistException(Class clazz, String field, String val) { + super(ChildrenExistException.generateMessage(clazz.getSimpleName(), field, val)); + } + + private static String generateMessage(String entity, String field, String val) { + return StringUtils.capitalize(entity) + + " with " + field + " "+ val + " Children Exist"; + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/exception/EntityExistException.java b/backend/platform-system/src/main/java/com/yfd/platform/system/exception/EntityExistException.java new file mode 100644 index 0000000..a3939ad --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/exception/EntityExistException.java @@ -0,0 +1,34 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.exception; + +import org.springframework.util.StringUtils; + +/** + * @author + * @date 2018-11-23 + */ +public class EntityExistException extends RuntimeException { + + public EntityExistException(Class clazz, String field, String val) { + super(EntityExistException.generateMessage(clazz.getSimpleName(), field, val)); + } + + private static String generateMessage(String entity, String field, String val) { + return StringUtils.capitalize(entity) + + " with " + field + " "+ val + " existed"; + } +} \ No newline at end of file diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/exception/EntityNotFoundException.java b/backend/platform-system/src/main/java/com/yfd/platform/system/exception/EntityNotFoundException.java new file mode 100644 index 0000000..714aa97 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/exception/EntityNotFoundException.java @@ -0,0 +1,34 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.exception; + +import org.springframework.util.StringUtils; + +/** + * @author + * @date 2018-11-23 + */ +public class EntityNotFoundException extends RuntimeException { + + public EntityNotFoundException(Class clazz, String field, String val) { + super(EntityNotFoundException.generateMessage(clazz.getSimpleName(), field, val)); + } + + private static String generateMessage(String entity, String field, String val) { + return StringUtils.capitalize(entity) + + " with " + field + " "+ val + " does not exist"; + } +} \ No newline at end of file diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/DataSourceController.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/DataSourceController.java new file mode 100644 index 0000000..12bb0d4 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/DataSourceController.java @@ -0,0 +1,42 @@ +package com.yfd.platform.system.system.controller; + +import com.yfd.platform.config.ResponseResult; +import com.yfd.platform.datasource.DataSource; +import com.yfd.platform.datasource.DataSourceAspect; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.annotation.Resource; + +/** + * @author zhengsl + * @since 2022-09-20 + */ +@RestController +@RequestMapping("/system") +@Tag(name = "切换数据库") +public class DataSourceController { + + @Resource + DataSourceAspect dataSourceAspect; + + /** + * 切换数据库 + * + * @DataSource(name="master") 可以通过注解方式切换数据库 + */ + @GetMapping("/changeDataSource") + @Operation(summary = "切换数据库") + public ResponseResult changeDataSource(Integer type) { + if (type == null) { + return ResponseResult.error("参数为空"); + } + String dataBase = dataSourceAspect.getDataBase(type); + String mess = "已切换为" + dataBase + "数据库"; + return ResponseResult.success(mess); + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/LoginController.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/LoginController.java new file mode 100644 index 0000000..32ed69a --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/LoginController.java @@ -0,0 +1,238 @@ +package com.yfd.platform.system.system.controller; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import cn.hutool.jwt.JWTUtil; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.wf.captcha.base.Captcha; +import com.yfd.platform.annotation.Log; +import com.yfd.platform.config.ResponseResult; +import com.yfd.platform.config.WebConfig; +import com.yfd.platform.config.bean.LoginCodeEnum; +import com.yfd.platform.config.bean.LoginProperties; +import com.yfd.platform.constant.Constant; +import com.yfd.platform.system.domain.LoginUser; +import com.yfd.platform.system.domain.SysLog; +import com.yfd.platform.system.domain.SysUser; +import com.yfd.platform.system.service.ISysLogService; +import com.yfd.platform.system.service.IUserService; +import com.yfd.platform.utils.RequestHolder; +import com.yfd.platform.utils.RsaUtils; +import com.yfd.platform.utils.StringUtils; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.web.bind.annotation.*; + +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import java.sql.Timestamp; +import java.util.HashMap; +import java.util.Map; + +/** + * @author TangWei + */ +@RestController +@RequestMapping("/user") +@Tag(name = "用户登录") +public class LoginController { + + @Autowired + private AuthenticationManager authenticationManager; + + @Autowired + private WebConfig webConfig; + + @Resource + private IUserService userService; + + @Value("${rsa.private_key}") + private String privateKey; + + @Resource + private ISysLogService sysLogService; + + @Resource + private LoginProperties loginProperties; + + @PostMapping("/login") + @Operation(summary = "登录用户") + @ResponseBody + public ResponseResult login(SysUser user) throws Exception { + // 密码解密 + String password = RsaUtils.decryptByPrivateKey(privateKey, + user.getPassword()); + + // 是否需要验证码不需要改成false + boolean hascode = true; + if (hascode) { + // 查询验证码 + String code = webConfig.loginuserCache().get(user.getUuid()); + // 清除验证码 + webConfig.loginuserCache().remove(user.getUuid()); + if (StrUtil.isBlank(code)) { + return ResponseResult.error("验证码不存在或已过期"); + } + if (StrUtil.isBlank(user.getCode()) || !user.getCode().equalsIgnoreCase(code)) { + return ResponseResult.error("验证码错误"); + } + } + //如果认证通过了,使用userId生成token token存入ResponseResult返回 + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(user.getUsername(), + password); + Authentication authenticate = + authenticationManager.authenticate(authenticationToken); + if (ObjectUtil.isNull(authenticate)) { + return ResponseResult.unlogin(); + } + LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); + Integer status = loginUser.getUser().getStatus(); + if ("0".equals(status.toString())) { + return ResponseResult.error("账号已停用"); + } + HttpServletRequest request = RequestHolder.getHttpServletRequest(); + SysLog sysLog = new SysLog(); + sysLog.setUsercode(user.getUsername()); + sysLog.setUsername(loginUser.getUser().getNickname()); + sysLog.setRequestip(StringUtils.getIp(request)); + sysLog.setBrowser(StringUtils.getBrowser(request)); + sysLog.setOpttype("登录(login)"); + sysLog.setModule("用户登录"); + String className = this.getClass().getName(); + String method = + Thread.currentThread().getStackTrace()[1].getMethodName(); + sysLog.setMethod(className + "." + method + "()"); + //sysLog.setParams(user.toString()); + sysLog.setDescription(loginUser.getUser().getNickname() + "登录系统!"); + sysLog.setLogtime(new Timestamp(System.currentTimeMillis())); + sysLogService.save(sysLog); + String userId = loginUser.getUser().getId(); + Map map = new HashMap(10) { + private static final long serialVersionUID = 1L; + + { + put("userid", userId); + put("username", loginUser.getUsername()); + long expireTime = + System.currentTimeMillis() + (long) (30L * 24L * 60L * 60L * 1000L); + put("expire_time", expireTime);//个月过期 + } + }; + + String token = JWTUtil.createToken(map, "12345678".getBytes()); + map.put("token", token); + //把完整的用户信息存入到HuTool缓存中,userId作为key + String jsonStr = JSONUtil.toJsonStr(loginUser); + webConfig.loginuserCache().put("login:" + userId, jsonStr); + webConfig.loginuserCache().put("expire_time:" + userId, map.get("expire_time").toString()); + return ResponseResult.successData(map); + } + + @Operation(summary = "获取验证码") + @GetMapping(value = "/code") + public ResponseResult getCode() { + // 获取运算的结果 + Captcha captcha = loginProperties.getCaptcha(); + String uuid = Constant.CODE_KEY + IdUtil.simpleUUID(); + //当验证码类型为 arithmetic时且长度 >= 2 时,captcha.text()的结果有几率为浮点型 + String captchaValue = captcha.text(); + if (captcha.getCharType() - 1 == LoginCodeEnum.arithmetic.ordinal() && captchaValue.contains(".")) { + captchaValue = captchaValue.split("\\.")[0]; + } + // 保存 + //redisUtils.set(uuid, captchaValue, loginProperties.getLoginCode() + // .getExpiration(), TimeUnit.MINUTES); + // 将验证码放入缓存,设置失效时间为60秒 + webConfig.loginuserCache().put(uuid, captchaValue, + Constant.CODE_EXPIRATION_TIME); + // 验证码信息 + Map imgResult = new HashMap(2) {{ + put("img", captcha.toBase64()); + put("uuid", uuid); + }}; + return ResponseResult.successData(imgResult); + } + + @PostMapping("/logout") + @Operation(summary = "退出登录") + @ResponseBody + public ResponseResult logout() { + //获取SecurityContextHolder中的用户id + UsernamePasswordAuthenticationToken authentication = + (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + LoginUser loginuser = (LoginUser) authentication.getPrincipal(); + String userId = loginuser.getUser().getId(); + //删除redis中的登陆用户信息 + webConfig.loginuserCache().remove("login:" + userId); + //记录退出日志 + HttpServletRequest request = RequestHolder.getHttpServletRequest(); + SysLog sysLog = new SysLog(); + sysLog.setUsercode(loginuser.getUsername()); + sysLog.setUsername(loginuser.getUser().getNickname()); + sysLog.setRequestip(StringUtils.getIp(request)); + sysLog.setBrowser(StringUtils.getBrowser(request)); + sysLog.setOpttype("其他(other)"); + sysLog.setModule("注销退出"); + sysLog.setDescription("注销退出系统!"); + sysLog.setLogtime(new Timestamp(System.currentTimeMillis())); + sysLogService.save(sysLog); + return ResponseResult.success(); + } + + @Log(module = "用户登录", value = "更改用户密码") + @GetMapping("/updatePassword") + @Operation(summary = "更改用户密码") + @ResponseBody + public ResponseResult updatePassword(@RequestBody SysUser user) throws Exception { + // 密码解密 + String password = RsaUtils.decryptByPrivateKey(privateKey, + user.getPassword()); + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + String cryptPassword = passwordEncoder.encode(password); + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + updateWrapper.set("password", cryptPassword); + updateWrapper.eq("id", user.getId()); + userService.update(updateWrapper); + return ResponseResult.success(); + } + + @GetMapping("/me") + @Operation(summary = "查询当前用户信息") + @ResponseBody + public ResponseResult getUserInfo() { + ResponseResult responseResult = userService.getLoginUserInfo(); + return ResponseResult.successData(responseResult); + } + + @Log(module = "用户登录", value = "修改个人信息") + @PostMapping("/updatePersonalInfo") + @Operation(summary = "修改个人信息") + @ResponseBody + public ResponseResult updateUser(@org.springframework.web.bind.annotation.RequestBody SysUser user) { + if (StrUtil.isEmpty(user.getId())) { + return ResponseResult.error("没有用户ID"); + } + //填写 当前用户名称 + user.setLastmodifier(userService.getUsername()); + //填写 当前日期 + user.setLastmodifydate(new Timestamp(System.currentTimeMillis())); + boolean ok = userService.updateById(user); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/MessageController.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/MessageController.java new file mode 100644 index 0000000..20d0ec5 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/MessageController.java @@ -0,0 +1,150 @@ +package com.yfd.platform.system.system.controller; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.yfd.platform.annotation.Log; +import com.yfd.platform.config.MessageConfig; +import com.yfd.platform.config.ResponseResult; +import com.yfd.platform.config.WebConfig; +import com.yfd.platform.system.domain.Message; +import com.yfd.platform.system.service.IMessageService; +import com.yfd.platform.system.service.IUserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.*; + +import jakarta.annotation.Resource; +import java.sql.Timestamp; +import java.util.*; + +/** + *

+ * 消息通知 前端控制器 + *

+ * + * @author TangWei + * @since 2023-03-19 + */ +@RestController +@RequestMapping("/system/message") +@Tag(name = "消息通知") +public class MessageController { + + @Resource + private IMessageService messageService; + + @Resource + private MessageConfig messageConfig; + + @Operation(summary = "查询消息") + @GetMapping("/getMessageList") + public ResponseResult getMessageList(Page page, + String status, String title, + String type, String startDate, + String endDate) { + if (StrUtil.isBlank(status)) { + return ResponseResult.error("参数为空"); + } + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + if ("0".equals(status)) { + queryWrapper.eq(Message::getStatus, "1"); + } else { + List statusList = new ArrayList<>(); + statusList.add("2"); + statusList.add("9"); + queryWrapper.in(Message::getStatus, statusList); + + if (StrUtil.isNotBlank(title)) { + queryWrapper.like(Message::getTitle, title); + } + + if (StrUtil.isNotBlank(type)) { + queryWrapper.eq(Message::getType, type); + } + + DateTime parseStartDate = DateUtil.parse(startDate); + DateTime parseEndDate = DateUtil.parse(endDate); + DateTime dateTime = DateUtil.offsetDay(parseEndDate, 1); + + if (parseStartDate != null && parseEndDate != null) { + queryWrapper.ge(Message::getCreatetime, parseStartDate).lt(Message::getCreatetime, dateTime); + } + } + queryWrapper.orderByDesc(Message::getCreatetime); + Page pageList = messageService.page(page, queryWrapper); + return ResponseResult.successData(pageList); + } + + @Operation(summary = "根据ID查询消息") + @GetMapping("/getMessageById") + public ResponseResult getMessageById(String id) { + if (StrUtil.isBlank(id)) { + return ResponseResult.error("参数为空"); + } + Message message = messageService.getById(id); + Map map = new HashMap<>(); + map.put("title", message.getTitle()); + map.put("content", message.getContent()); + map.put("createtime", message.getCreatetime()); + return ResponseResult.successData(map); + } + + @Log(module = "消息通知",value = "根据ID删除消息") + @Operation(summary = "根据ID删除消息") + @PostMapping("/deleteMessageById") + public ResponseResult deleteMessageById(@RequestParam String id) { + if (StrUtil.isBlank(id)) { + return ResponseResult.error("参数为空"); + } + String[] split = id.split(","); + List idList = Arrays.asList(split); + boolean ok = messageService.removeByIds(idList); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error("删除失败"); + } + + } + + @Log(module = "消息通知", value = "将消息标记为已阅状态") + @Operation(summary = "标记已阅") + @PostMapping("/setMessageStatus") + public ResponseResult setMessageStatus(@RequestParam String id) { + if (StrUtil.isBlank(id)) { + return ResponseResult.error("参数为空"); + } + String[] split = id.split(","); + long time = System.currentTimeMillis(); + for (String mid : split) { + Message message = messageService.getById(mid); + if ("9".equals(message.getStatus())) { + continue; + } + message.setStatus("2"); + message.setReadtime(new Timestamp(time)); + messageService.updateById(message); + } + messageConfig.sendMessage(); + return ResponseResult.success(); + } + + @Operation(summary = "全部已阅") + @PostMapping("/setAllMessageStatus") + public ResponseResult setAllMessageStatus() { + long time = System.currentTimeMillis(); + List list = + messageService.list(new LambdaQueryWrapper().eq(Message::getStatus, "1")); + for (Message message : list) { + message.setStatus("2"); + message.setReadtime(new Timestamp(time)); + messageService.updateById(message); + } + messageConfig.sendMessage(); + return ResponseResult.success(); + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/QuartzJobController.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/QuartzJobController.java new file mode 100644 index 0000000..07e20fd --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/QuartzJobController.java @@ -0,0 +1,183 @@ +package com.yfd.platform.system.system.controller; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.yfd.platform.annotation.Log; +import com.yfd.platform.config.ResponseResult; +import com.yfd.platform.system.domain.QuartzJob; +import com.yfd.platform.system.service.IQuartzJobService; +import com.yfd.platform.system.service.impl.UserServiceImpl; +import com.yfd.platform.utils.QuartzManage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.quartz.CronExpression; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import jakarta.annotation.Resource; +import java.sql.Timestamp; +import java.time.LocalDateTime; + +/** + *

+ * 定时任务 前端控制器 + *

+ * + * @author TangWei + * @since 2023-03-19 + */ +@RestController +@RequestMapping("/system/quartzjob") +@Tag(name = "定时任务") +@Transactional +public class QuartzJobController { + + @Resource + private IQuartzJobService quartzJobService; + + @Resource + private UserServiceImpl currentUser; + + @Resource + private QuartzManage quartzManage; + + @Operation(summary = "查询定时任务") + @GetMapping("/getQuartzJobList") + public ResponseResult getQuartzJobList(Page page, + String jobName) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + if (StrUtil.isNotBlank(jobName)) { + queryWrapper.like(QuartzJob::getJobName, jobName); + } + queryWrapper.orderByAsc(QuartzJob::getOrderno); + Page pageList = quartzJobService.page(page, queryWrapper); + return ResponseResult.successData(pageList); + } + + @Log(module = "定时任务管理", value = "新增定时任务") + @Operation(summary = "新增定时任务") + @PostMapping("/addQuartzJob") + public ResponseResult addQuartzJob(@RequestBody QuartzJob quartzJob) { + if (quartzJob == null) { + return ResponseResult.error("参数为空"); + } + // 添加最近修改人 + quartzJob.setLastmodifier(currentUser.getUsername()); + // 添加最近修改时间 + quartzJob.setLastmodifydate(new Timestamp(System.currentTimeMillis())); + if (StrUtil.isBlank(quartzJob.getJobCron()) || !CronExpression.isValidExpression(quartzJob.getJobCron())) { + return ResponseResult.error("cron表达式格式错误"); + } + quartzJob.setStatus("0"); + boolean ok = quartzJobService.addQuartzJob(quartzJob); + quartzManage.addJob(quartzJob); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error("新增失败"); + } + } + + @Log(module = "定时任务管理", value = "设置定时任务是否有效") + @Operation(summary = "设置定时任务是否有效") + @PostMapping("/setQuartzStatus") + public ResponseResult setQuartzStatus(@RequestParam String id, + @RequestParam String status) { + if (StrUtil.isBlank(id) || StrUtil.isBlank(status)) { + return ResponseResult.error("参数为空"); + } + LambdaUpdateWrapper updateWrapper = + new LambdaUpdateWrapper<>(); + //根据id 更新状态,最近修改人,最近修改时间 + updateWrapper.eq(QuartzJob::getId, id).set(QuartzJob::getStatus, + status).set( + QuartzJob::getLastmodifier, currentUser.getUsername()).set(QuartzJob::getLastmodifydate, + LocalDateTime.now()); + boolean ok = quartzJobService.update(updateWrapper); + QuartzJob quartzJob = quartzJobService.getById(id); + if ("0".equals(quartzJob.getStatus())) { + quartzManage.pauseJob(quartzJob); + } else { + quartzManage.resumeJob(quartzJob); + } + + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + @Operation(summary = "根据ID查询定时任务") + @GetMapping("/getQuartzJobById") + public ResponseResult getQuartzJobById(String id) { + QuartzJob quartzJob = quartzJobService.getById(id); + return ResponseResult.successData(quartzJob); + } + + @Log(module = "定时任务管理", value = "修改定时任务") + @Operation(summary = "修改定时任务") + @PostMapping("/updateQuartzJob") + @Transactional(rollbackFor = Exception.class) + public ResponseResult updateQuartzJob(@RequestBody QuartzJob quartzJob) { + // 添加最近修改人 + quartzJob.setLastmodifier(currentUser.getUsername()); + // 添加最近修改时间 + quartzJob.setLastmodifydate(new Timestamp(System.currentTimeMillis())); + if (StrUtil.isBlank(quartzJob.getJobCron()) || !CronExpression.isValidExpression(quartzJob.getJobCron())) { + return ResponseResult.error("cron表达式格式错误"); + } + boolean ok = quartzJobService.updateById(quartzJob); + quartzManage.updateJobCron(quartzJob); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error("修改失败"); + } + } + + @Log(module = "定时任务管理", value = "删除定时任务") + @Operation(summary = "删除定时任务") + @PostMapping("/deleteQuartzJob") + public ResponseResult deleteQuartzJob(@RequestParam String id) { + if (StrUtil.isBlank(id)) { + return ResponseResult.error("参数为空"); + } + boolean ok = quartzJobService.deleteQuartzJob(id); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error("删除失败"); + } + } + + @Log(module = "定时任务管理", value = "执行定时任务") + @Operation(summary = "执行定时任务") + @PostMapping("/execution") + public ResponseResult execution(@RequestParam String id) { + quartzJobService.execution(quartzJobService.getById(id)); + return ResponseResult.success(); + } + + /********************************** + * 用途说明: 拖动修改定时顺序 + * 参数说明 fromID 当前ID toID 到达ID + * 返回值说明: com.yfd.platform.config.ResponseResult 成功或者失败 + ***********************************/ + @Log(module = "定时任务管理", value = "拖动定时任务") + @PostMapping("/changeDictOrder") + @Operation(summary = "拖动修改定时任务顺序") + public ResponseResult changeQuartzOrder(@RequestParam String fromID, + @RequestParam String toID) { + + boolean ok = quartzJobService.changeDictOrder(fromID, toID); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SSEController.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SSEController.java new file mode 100644 index 0000000..9398d9f --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SSEController.java @@ -0,0 +1,59 @@ +package com.yfd.platform.system.system.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.yfd.platform.component.ServerSendEventServer; +import com.yfd.platform.config.WebConfig; +import com.yfd.platform.constant.Constant; +import com.yfd.platform.system.domain.Message; +import com.yfd.platform.system.service.IMessageService; +import com.yfd.platform.system.service.IUserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import jakarta.annotation.Resource; + +/** + * @author Huhailong + */ +@Slf4j +@RestController +@CrossOrigin +@RequestMapping("/sse") +@Tag(name = "SSE推送服务") +public class SSEController { + + @Resource + private IMessageService messageService; + + @GetMapping("/connect/{token}") + @Operation(summary = "建立连接") + public SseEmitter connect(@PathVariable String token) { + SseEmitter connect = ServerSendEventServer.connect(token); + long count = + messageService.count(new LambdaQueryWrapper().eq(Message::getStatus, "1")); + ServerSendEventServer.sendMessage(token, count + ""); + return connect; + } + + @GetMapping("/sendmsg") + @Operation(summary = "发送消息") + public void sendMessage(String token, String message) throws InterruptedException { + + ServerSendEventServer.sendMessage(token, message); + } + + @GetMapping("/sendgroupmsg") + @Operation(summary = "多人发送消息") + public void sendgroupmsg(String groupid, String message) throws InterruptedException { + ServerSendEventServer.groupSendMessage(groupid, message); + } + + @GetMapping("/disconnect/{token}") + @Operation(summary = "关闭连接") + public void disconnect(@PathVariable String token) throws InterruptedException { + ServerSendEventServer.removeUser(token); + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysConfigController.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysConfigController.java new file mode 100644 index 0000000..1edb974 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysConfigController.java @@ -0,0 +1,68 @@ +package com.yfd.platform.system.system.controller; + + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import com.yfd.platform.config.ResponseResult; +import com.yfd.platform.system.domain.SysConfig; +import com.yfd.platform.system.service.ISysConfigService; +import com.yfd.platform.system.service.IUserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.*; + +import jakarta.annotation.Resource; +import javax.sound.sampled.UnsupportedAudioFileException; +import java.io.IOException; +import java.sql.Timestamp; + + +/** + *

+ * 系统全局配置 前端控制器 + *

+ * + * @author zhengsl + * @since 2022-01-19 + */ +@RestController + @RequestMapping("/system/config") +@Tag(name = "系统全局配置") +public class SysConfigController { + @Resource + private ISysConfigService configService; + + @Resource + private IUserService userService; + + @PostMapping("/getOneById") + @Operation(summary = "根据id查询全局配置详情记录") + @ResponseBody + public SysConfig getOneById(String id){ + return configService.getById(id); + } + + @PostMapping("/addConfig") + @Operation(summary = "根据id查询全局配置详情记录") + @ResponseBody + public ResponseResult addConfig(@RequestBody SysConfig config ) throws IOException, UnsupportedAudioFileException { + if (StrUtil.isEmpty(config.getId())){ + config.setId(IdUtil.fastSimpleUUID()); } + config.setLastmodifier(userService.getUsername()); + config.setLastmodifydate(new Timestamp(System.currentTimeMillis())); + boolean ok=configService.save(config); + return ResponseResult.success(); + } + + @PostMapping("/updateById") + @Operation(summary = "根据id修改全局配置记录") + @ResponseBody + public ResponseResult updateById(@RequestBody SysConfig config) throws IOException, UnsupportedAudioFileException { + config.setLastmodifier(userService.getUsername()); + config.setLastmodifydate(new Timestamp(System.currentTimeMillis())); + boolean ok=configService.updateById(config); + return ResponseResult.success(); + } + + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysDictionaryController.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysDictionaryController.java new file mode 100644 index 0000000..48dc722 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysDictionaryController.java @@ -0,0 +1,142 @@ +package com.yfd.platform.system.system.controller; + +import cn.hutool.core.util.StrUtil; +import com.yfd.platform.annotation.Log; +import com.yfd.platform.config.ResponseResult; +import com.yfd.platform.system.domain.SysDictionary; +import com.yfd.platform.system.mapper.SysDictionaryItemsMapper; +import com.yfd.platform.system.service.ISysDictionaryService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.*; + +import jakarta.annotation.Resource; +import java.util.List; +import java.util.Objects; + +/** + *

+ * 数据字典表 前端控制器 + *

+ * + * @author TangWei + * @since 2023-03-08 + */ +@RestController +@RequestMapping("/system/dictionary") +@Tag(name = "数据字典") +public class SysDictionaryController { + + @Resource + private ISysDictionaryService sysDictionaryService; + + /********************************** + * 用途说明: 获取数据字典列表 + * 参数说明 dictType 字典类型 + * 返回值说明: com.yfd.platform.config.ResponseResult 返回分页查询结果 + ***********************************/ + @GetMapping("/dictList") + @Operation(summary = "获取数据字典列表") + public ResponseResult getDictList(String dictType) { + if (StrUtil.isBlank(dictType)) { + return ResponseResult.error("参数为空"); + } + List sysDictionaries = + sysDictionaryService.getDictList(dictType); + return ResponseResult.successData(sysDictionaries); + } + + /********************************** + * 用途说明: 根据ID删除字典 + * 参数说明 id 字典ID + * 返回值说明: com.yfd.platform.config.ResponseResult 返回删除结果成功或者失败 + ***********************************/ + @Log(module = "数据字典", value = "根据ID删除字典") + @PostMapping("/deleteById") + @Operation(summary = "根据ID删除字典") + public ResponseResult deleteDictById(@RequestParam String id) { + boolean ok = sysDictionaryService.deleteDictById(id); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /********************************** + * 用途说明: 新增字典 + * 参数说明 sysDictionary 字典对象 + * 返回值说明: com.yfd.platform.config.ResponseResult 返回增加成功或者失败 + ***********************************/ + @Log(module = "数据字典", value = "新增数据字典") + @PostMapping("/addDict") + @Operation(summary = "新增字典") + public ResponseResult addDict(@RequestBody SysDictionary sysDictionary) { + if (sysDictionary == null) { + return ResponseResult.error("参数为空"); + } + boolean ok = sysDictionaryService.addDict(sysDictionary); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /********************************** + * 用途说明: 修改字典 + * 参数说明 sysDictionary 字典对象 + * 返回值说明: com.yfd.platform.config.ResponseResult 返回修改成功或者失败 + ***********************************/ + @Log(module = "数据字典", value = "修改数据字典") + @PostMapping("/updateDict") + @Operation(summary = "修改字典") + public ResponseResult updateDict(@RequestBody SysDictionary sysDictionary) { + if (sysDictionary == null) { + return ResponseResult.error("参数为空"); + } + boolean ok = sysDictionaryService.updateById(sysDictionary); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /********************************** + * 用途说明: 根据ID查询字典 + * 参数说明 sysDictionary 字典对象 + * 返回值说明: com.yfd.platform.config.ResponseResult 返回查询结果 + ***********************************/ + @PostMapping("/getDictById") + @Operation(summary = "根据ID查询字典") + public ResponseResult getDictById(String id) { + if (StrUtil.isBlank(id)) { + return ResponseResult.error("参数为空"); + } + SysDictionary sysDictionary = sysDictionaryService.getById(id); + return ResponseResult.successData(sysDictionary); + } + + /********************************** + * 用途说明: 拖动修改字典顺序 + * 参数说明 fromID 当前ID toID 到达ID + * 返回值说明: com.yfd.platform.config.ResponseResult 返回拖动成功或者失败 + ***********************************/ + @Log(module = "数据字典", value = "拖动修改字典顺序") + @PostMapping("/changeDictOrder") + @Operation(summary = "拖动修改字典顺序") + public ResponseResult changeDictOrder(@RequestParam String fromID, + @RequestParam String toID) { + + boolean ok = sysDictionaryService.changeDictOrder(fromID, toID); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysDictionaryItemsController.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysDictionaryItemsController.java new file mode 100644 index 0000000..ee4e9e5 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysDictionaryItemsController.java @@ -0,0 +1,201 @@ +package com.yfd.platform.system.system.controller; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.yfd.platform.annotation.Log; +import com.yfd.platform.config.ResponseResult; +import com.yfd.platform.system.domain.SysDictionaryItems; +import com.yfd.platform.system.mapper.SysDictionaryItemsMapper; +import com.yfd.platform.system.service.ISysDictionaryItemsService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.*; + +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Arrays; +import java.util.List; + +/** + *

+ * 数据字典明细 前端控制器 + *

+ * + * @author TangWei + * @since 2023-03-08 + */ +@RestController +@RequestMapping("/system/dictionaryItems") +@Tag(name = "数据字典项") +public class SysDictionaryItemsController { + + @Resource + private ISysDictionaryItemsService sysDictionaryItemsService; + + /********************************** + * 用途说明: 分页查询字典项信息 + * 参数说明 dictID 字典ID ItemName 字典项名称 pageNum 当前页 + * 返回值说明: com.yfd.platform.config.ResponseResult 返回分页查询结果 + ***********************************/ + @GetMapping("/page") + @Operation(summary = "分页查询字典项信息") + public ResponseResult getDictItemPage(String dictId, String dictName, + Page page) { + + LambdaQueryWrapper queryWrapper = + new LambdaQueryWrapper<>(); + queryWrapper.eq(SysDictionaryItems::getDictId, dictId).orderByAsc(SysDictionaryItems::getOrderNo); + + // 查询前将序号初始化 + List list = + sysDictionaryItemsService.list(queryWrapper); + for (int i = 0; i < list.size(); i++) { + SysDictionaryItems sysDictionaryItems = list.get(i); + sysDictionaryItems.setOrderNo(i + 1); + sysDictionaryItemsService.updateById(sysDictionaryItems); + } + Page sysDictionaryItemsPage = + sysDictionaryItemsService.getDictItemPage(dictId, dictName, + page); + + return ResponseResult.successData(sysDictionaryItemsPage); + } + + /********************************** + * 用途说明: 增加字典项 + * 参数说明 sysDictionaryItems 字典项信息 + * 返回值说明: com.yfd.platform.config.ResponseResult 返回增加成功或者失败 + ***********************************/ + @Log(module = "数据字典项", value = "增加字典项") + @PostMapping("/addDictionaryItem") + @Operation(summary = "增加字典项") + public ResponseResult addDictionaryItem(@RequestBody SysDictionaryItems sysDictionaryItems) { + if (sysDictionaryItems == null) { + return ResponseResult.error("参数为空"); + } + boolean ok = + sysDictionaryItemsService.addDictionaryItem(sysDictionaryItems); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /********************************** + * 用途说明: 修改字典项 + * 参数说明 sysDictionaryItems 字典项信息 + * 返回值说明: com.yfd.platform.config.ResponseResult 返回修改成功或者失败 + ***********************************/ + @Log(module = "数据字典项", value = "修改字典项") + @PostMapping("/updateDictionaryItem") + @Operation(summary = "修改字典项") + public ResponseResult updateDictionaryItem(@RequestBody SysDictionaryItems sysDictionaryItems) { + if (sysDictionaryItems == null) { + return ResponseResult.error("参数为空"); + } + boolean ok = + sysDictionaryItemsService.updateById(sysDictionaryItems); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /********************************** + * 用途说明: 根据ID查询字典项 + * 参数说明 id 字典项ID + * 返回值说明: com.yfd.platform.config.ResponseResult 返回字典项信息 + ***********************************/ + @GetMapping("/getDictItemById") + @Operation(summary = "根据ID查询字典项") + public ResponseResult getDictItemById(String id) { + if (StrUtil.isBlank(id)) { + return ResponseResult.error("参数为空"); + } + SysDictionaryItems sysDictionaryItems = + sysDictionaryItemsService.getById(id); + return ResponseResult.successData(sysDictionaryItems); + } + + /********************************** + * 用途说明: 根据ID删除字典项 + * 参数说明 id 字典项ID + * 返回值说明: com.yfd.platform.config.ResponseResult 返回删除成功或者失败 + ***********************************/ + @Log(module = "数据字典项", value = "根据ID删除字典项") + @PostMapping("/deleteDictItemById") + @Operation(summary = "根据ID删除字典项") + public ResponseResult deleteDictItemById(@RequestParam String id) { + if (StrUtil.isBlank(id)) { + return ResponseResult.error("参数为空"); + } + boolean ok = sysDictionaryItemsService.removeById(id); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /********************************** + * 用途说明: 批量删除字典项 + * 参数说明 ids 字典项id数组 + * 返回值说明: com.yfd.platform.config.ResponseResult 返回批量删除成功或失败 + ***********************************/ + @Log(module = "数据字典项", value = "批量删除字典项") + @PostMapping("/deleteDictItemByIds") + @Operation(summary = "批量删除字典项") + public ResponseResult deleteDictItemByIds(@RequestParam String id) { + if (StrUtil.isBlank(id)) { + return ResponseResult.error("参数为空"); + } + String[] splitIds = id.split(","); + // 数组转集合 + List ids = Arrays.asList(splitIds); + boolean ok = sysDictionaryItemsService.removeByIds(ids); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /********************************** + * 用途说明: 拖动修改字典项顺序 + * 参数说明 fromID 当前ID toID 到达ID + * 返回值说明: com.yfd.platform.config.ResponseResult 返回拖动成功或者失败 + ***********************************/ + @Log(module = "数据字典项", value = "拖动修改字典项顺序") + @PostMapping("/changeItemOrder") + @Operation(summary = "拖动修改字典项顺序") + public ResponseResult changeItemOrder(@RequestParam String fromID, + @RequestParam String toID) { + boolean ok = sysDictionaryItemsService.changeItemOrder(fromID, toID); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /********************************** + * 用途说明: 导出数据字典项数据 + * 参数说明 sysDictionaryItemsList 所需导出的字典项集合 + * 返回值说明: com.yfd.platform.config.ResponseResult 返回导出成功或失败 + ***********************************/ + @Log(module = "数据字典项", value = "导出字典数据到Excel") + @GetMapping("/exportExcel") + @Operation(summary = "导出数据字典项数据") + public void exportExcel(String dictID, String itemName, + Page page, + HttpServletResponse response) { + Page sysDictionaryItemsPage = + sysDictionaryItemsService.getDictItemPage(dictID, itemName, + page); + sysDictionaryItemsService.exportExcel(sysDictionaryItemsPage.getRecords(), response); + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysLogController.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysLogController.java new file mode 100644 index 0000000..d876fac --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysLogController.java @@ -0,0 +1,74 @@ +package com.yfd.platform.system.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.yfd.platform.annotation.Log; +import com.yfd.platform.config.ResponseResult; +import com.yfd.platform.system.domain.SysLog; +import com.yfd.platform.system.service.ISysLogService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.web.bind.annotation.*; + +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + *

+ * 系统操作日志 前端控制器 + *

+ * + * @author TangWei + * @since 2023-03-08 + */ +@RestController +@RequestMapping("/system/log") +@Tag(name = "系统日志") +public class SysLogController { + + @Resource + private ISysLogService sysLogService; + + /********************************** + * 用途说明: 分页查询日志信息 + * 参数说明 page分页对象、username(用户名)、(optType) + * 操作类型、startDate(开始日期)、endDate(结束日期) + * 返回值说明: com.yfd.platform.config.ResponseResult 返回分页查询结果 + ***********************************/ + @PostMapping("/getLogList") + @Operation(summary = "分页查询日志信息") + public ResponseResult getLogList(String username, String optType, + String startDate, + String endDate, Page page) { + + Page sysLogPage = sysLogService.getLogList(username, optType, + startDate, endDate, page); + Map map = new HashMap<>(); + map.put("list", sysLogPage.getRecords()); + map.put("total", sysLogPage.getTotal()); + map.put("size", sysLogPage.getSize()); + map.put("current", sysLogPage.getCurrent()); + return ResponseResult.successData(map); + } + + /********************************** + * 用途说明: 导出日志数据 + * 参数说明 sysLogs 所需导出的字典项集合 + * 返回值说明: com.yfd.platform.config.ResponseResult 返回导出成功或者失败 + ***********************************/ + @Log(module = "系统日志", value = "导出系统日志到Excel") + @GetMapping("/exportExcel") + @Operation(summary = "导出日志数据") + public void exportExcel(String username, String optType, + String startDate, + String endDate, Page page, + HttpServletResponse response) throws IOException { + + Page sysLogPage = sysLogService.getLogList(username, optType, + startDate, endDate, page); + sysLogService.exportExcel(sysLogPage.getRecords(), response); + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysMenuController.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysMenuController.java new file mode 100644 index 0000000..a20899f --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysMenuController.java @@ -0,0 +1,310 @@ +package com.yfd.platform.system.system.controller; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.yfd.platform.annotation.Log; +import com.yfd.platform.config.ResponseResult; +import com.yfd.platform.system.domain.SysMenu; +import com.yfd.platform.system.domain.SysUser; +import com.yfd.platform.system.service.ISysMenuService; +import com.yfd.platform.system.service.IUserService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import org.apache.catalina.User; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.annotation.Resource; +import java.io.File; +import java.io.FileNotFoundException; +import java.sql.Timestamp; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + *

+ * 菜单及按钮 前端控制器 + *

+ * + * @author zhengsl + * @since 2021-12-15 + */ +@RestController +@RequestMapping("/system/menu") +@Tag(name = "菜单及按钮") +public class SysMenuController { + + @Resource + private ISysMenuService sysMenuService; + + @Resource + private IUserService userService; + + // 菜单图片路径通过服务层配置获取,无需在控制器注入 + + /*********************************** + * 用途说明:获取菜单结构树(含按钮) + * 参数说明 + * systemcode 系统 + * name 名称 + * isdisplay 是否显示 + * 返回值说明: 菜单结构树集合 + ***********************************/ + @PostMapping("/getMenuButtonTree") + @Operation(summary = "获取菜单结构树(含按钮)") + @ResponseBody + public List> getMenuButtonTree(String systemcode, + String name, + String isdisplay) { + return sysMenuService.getMenuButtonTree(systemcode, name, isdisplay); + } + + /*********************************** + * 用途说明:获取菜单结构树(不含按钮) + * 参数说明 + * systemcode 系统 + * name 名称 + * isdisplay 是否显示 + * 返回值说明: 菜单结构树集合 + ***********************************/ + @PostMapping("/getMenuTree") + @Operation(summary = "获取菜单结构树(不含按钮)") + @ResponseBody + public List> getMenuTree(String systemcode, + String name, + String isdisplay) { + return sysMenuService.getMenuTree(systemcode, name, isdisplay); + } + + /*********************************** + * 用途说明:权限分配 + * 参数说明 + * systemcode 系统 + * name 名称 + * isdisplay 是否显示 + * 返回值说明: 菜单结构树集合 + ***********************************/ + @PostMapping("/permissionAssignment") + @Operation(summary = "获取分配权限(不含按钮)") + @ResponseBody + public List> permissionAssignment(String code, String roleId) { + if (StrUtil.isBlank(code)) { + code = "1"; + } + return sysMenuService.permissionAssignment(code,roleId); + } + + /********************************** + * 用途说明: 获取当前用户菜单结构树 + * 参数说明 + * 返回值说明: java.util.List + ***********************************/ + @GetMapping("/treeRoutes") + @Operation(summary = "获取当前用户菜单结构树") + @ResponseBody + public List> getMenuTreeByUser() { + SysUser userInfo = userService.getUserInfo(); + String id = ""; + if (0 != userInfo.getUsertype()) { + id = userInfo.getId(); + } + return sysMenuService.getMenuTree(id); + } + + /*********************************** + * 用途说明:根据id查询菜单或按钮详情 + * 参数说明 + * id 菜单或按钮表id + * 返回值说明: 菜单或按钮表对象 + ***********************************/ + @PostMapping("/getOneById") + @Operation(summary = "根据id查询菜单或按钮详情") + @ResponseBody + public ResponseResult getOneById(String id) { + SysMenu sysMenu = sysMenuService.getById(id); + return ResponseResult.successData(sysMenu); + } + + /*********************************** + * 用途说明:新增菜单及按钮 + * 参数说明 + * sysMenu 菜单或按钮表对象 + * 返回值说明: 是否添加成功提示 + ***********************************/ + @Log(module = "菜单及按钮", value = "新增菜单及按钮!") + @PostMapping("/addMenu") + @Operation(summary = "新增菜单及按钮") + @ResponseBody + public ResponseResult addMenu(@RequestBody SysMenu sysMenu) { + boolean isOk = sysMenuService.addMenu(sysMenu); + if (isOk) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /*********************************** + * 用途说明:修改菜单及按钮 + * 参数说明 + * sysMenu 菜单或按钮表对象 + * 返回值说明: 是否修改成功提示 + ***********************************/ + @Log(module = "菜单及按钮", value = "修改菜单及按钮") + @PostMapping("/updateById") + @Operation(summary = "修改菜单及按钮") + @ResponseBody + public ResponseResult updateById(@RequestBody SysMenu sysMenu) { + sysMenu.setLastmodifier(userService.getUsername()); + sysMenu.setLastmodifydate(new Timestamp(System.currentTimeMillis())); + boolean isOk = sysMenuService.updateById(sysMenu); + if (isOk) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /*********************************** + * 用途说明:根据id删除单个图标 + * 参数说明 + * id 删除图标id + * icon 图标名称 + * 返回值说明: 是否删除成功 + ***********************************/ + @Log(module = "菜单及按钮", value = "根据id删除单个图标!") + @PostMapping("/deleteIcon") + @Operation(summary = "根据id删除单个图标") + @ResponseBody + public ResponseResult deleteIcon(@RequestParam String id) { + boolean ok = sysMenuService.deleteIcon(id); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /*********************************** + * 用途说明:更新菜单及按钮是否有效 + * 参数说明 + * id 菜单及按钮表id + * isdisplay 是否有效字段 + * 返回值说明: 是否更新成功 + ***********************************/ + @Log(module = "菜单及按钮", value = "更新菜单及按钮是否有效!") + @PostMapping("/setIsDisplay") + @Operation(summary = "更新菜单及按钮是否有效") + @ResponseBody + public ResponseResult setIsDisplay(String id, String isdisplay) { + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + //根据id 修改是否显示 ,最近修改人,最近修改时间 + updateWrapper.eq("id", id).set("isdisplay", isdisplay).set( + "lastmodifier", userService.getUsername()).set( + "lastmodifydate", + new Timestamp(System.currentTimeMillis())); + boolean ok = sysMenuService.update(updateWrapper); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /*********************************** + * 用途说明:菜单及按钮序号排序 + * 参数说明 + * parentid 上级id + * orderMap map<菜单及按钮表id,排列序号> + * 返回值说明: 是否更新成功 + ***********************************/ + @Log(module = "菜单及按钮", value = "菜单及按钮序号排序!") + @PostMapping("/moveOrderno") + @Operation(summary = "菜单及按钮序号排序") + @ResponseBody + public ResponseResult moveOrderno(@RequestParam String parentid, + @RequestParam String id, + @RequestParam int orderno) { + boolean ok = sysMenuService.moveOrderno(parentid, id, orderno); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /*********************************** + * 用途说明:根据id删除菜单或按钮 + * 参数说明 + * id 删除列的id + * 返回值说明: 是否删除成功 + ***********************************/ + @Log(module = "菜单及按钮", value = "根据id删除菜单或按钮!") + @PostMapping("/deleteById") + @Operation(summary = "根据id删除菜单或按钮") + @ResponseBody + public ResponseResult deleteById(@RequestParam String id) { + boolean ok = sysMenuService.deleteById(id); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /********************************** + * 用途说明: 菜单或者按钮拖动 + * 参数说明 id + * 参数说明 id1 + * 返回值说明: com.yfd.platform.config.ResponseResult + ***********************************/ + @Log(module = "菜单及按钮", value = "拖动修改菜单或按钮同级顺序!") + @PostMapping("/changeMenuOrder") + @Operation(summary = "菜单或按钮切换") + @ResponseBody + public ResponseResult changeMenuOrder(@RequestParam String fromId, + @RequestParam String toId) { + if (StrUtil.isBlank(fromId) || StrUtil.isBlank(toId)) { + return ResponseResult.error("参数为空!"); + } + if (fromId.equals(toId)) { + return ResponseResult.error("切换失败!"); + } + boolean ok = sysMenuService.changeOderNoById(fromId, toId); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /*********************************** + * 用途说明:上传单个图标 + * 参数说明 + * icon 图标 + * 返回值说明: 是否上传成功 + ***********************************/ + @PostMapping("/uploadIcon") + @Operation(summary = "上传单个图标") + @ResponseBody + public ResponseResult uploadIcon(MultipartFile icon, String menuId) throws FileNotFoundException { + if (StrUtil.isNotBlank(menuId)) { + SysMenu sysMenu = sysMenuService.getById(menuId); + //图片路径 + String iconname = + System.getProperty("user.dir") + "\\src\\main" + + "\\resources\\static\\icon" + File.separator + sysMenu.getIcon(); + //删除图标 + new File(iconname).delete(); + } + String filename = sysMenuService.uploadIcon(icon); + SysMenu sysMenu = new SysMenu(); + sysMenu.setId(menuId); + sysMenu.setIcon(filename); + sysMenuService.updateById(sysMenu); + return ResponseResult.successData(filename); + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysOrganizationController.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysOrganizationController.java new file mode 100644 index 0000000..27dd726 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysOrganizationController.java @@ -0,0 +1,213 @@ +package com.yfd.platform.system.system.controller; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.yfd.platform.annotation.Log; +import com.yfd.platform.config.ResponseResult; +import com.yfd.platform.system.domain.SysOrganization; +import com.yfd.platform.system.domain.SysRole; +import com.yfd.platform.system.domain.SysUser; +import com.yfd.platform.system.mapper.SysRoleMapper; +import com.yfd.platform.system.service.ISysOrganizationService; +import com.yfd.platform.system.service.IUserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.*; + +import jakarta.annotation.Resource; +import java.sql.Timestamp; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + *

+ * 系统组织框架 前端控制器 + *

+ * + * @author zhengsl + * @since 2021-12-15 + */ +@RestController +@RequestMapping("/system/organization") +@Tag(name = "系统组织框架") +public class SysOrganizationController { + + @Resource + private ISysOrganizationService organizationService; + + @Resource + private IUserService userService; + + /*********************************** + * 用途说明:获取组织范围树结构 + * 参数说明 + *parentid 上级id + * params 名称(根据名称查询二级) + * 返回值说明: 组织树集合 + ***********************************/ + @PostMapping("/getOrgScopeTree") + @Operation(summary = "获取组织范围树结构") + @ResponseBody + public List> getOrgScopeTree(String roleId) { + return organizationService.getOrgScopeTree(roleId); + } + + /*********************************** + * 用途说明:获取组织范围 + * 参数说明 + * 返回值说明: 组织范围集合 + ***********************************/ + @PostMapping("/getOrgTree") + @Operation(summary = "获取组织结构树") + @ResponseBody + public List> getOrgTree(String parentid, + String params) { + return organizationService.getOrgTree(parentid, params); + } + + /*********************************** + * 用途说明:根据企业ID查询组织详情 + * 参数说明 + * id 企业id + * 返回值说明: 系统组织框架对象 + ***********************************/ + @PostMapping("/getOrganizationById") + @Operation(summary = "根据企业ID查询组织信息") + @ResponseBody + public ResponseResult getOrganizationById(String id, String orgName) { + if (StrUtil.isBlank(id)) { + return ResponseResult.error("查询失败!"); + } + List sysOrganizations = + organizationService.getOrganizationById(id, orgName); + return ResponseResult.successData(sysOrganizations); + } + + /*********************************** + * 用途说明:根据ID查询组织详情 + * 参数说明 + * id 系统组织id + * 返回值说明: 系统组织框架对象 + ***********************************/ + @PostMapping("/getOneById") + @Operation(summary = "根据ID查询组织详情") + @ResponseBody + public ResponseResult getOneById(String id) { + SysOrganization sysOrganization = organizationService.getById(id); + return ResponseResult.successData(sysOrganization); + } + + /*********************************** + * 用途说明:新增系统组织框架 + * 参数说明 + * sysOrganization 系统组织框架对象 + * 返回值说明: 是否新增成功 + ***********************************/ + @Log(module = "系统组织框架", value = "新增企业或者部门!") + @PostMapping("/addOrg") + @Operation(summary = "新增系统组织框架") + @ResponseBody + public ResponseResult addOrg(@RequestBody SysOrganization sysOrganization) { + //判断是否是否填写 有效 否则默认为 1 + if (StrUtil.isEmpty(sysOrganization.getIsvaild())) { + sysOrganization.setIsvaild("1"); + } + if("".equals(sysOrganization.getId())){ + sysOrganization.setId(null); + } + //填写 当前用户名称 + sysOrganization.setLastmodifier(userService.getUsername()); + //填写 当前日期 + sysOrganization.setLastmodifydate(new Timestamp(System.currentTimeMillis())); + //新增 系统组织R + boolean isOk = organizationService.addOrg(sysOrganization); + if (isOk) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /*********************************** + * 用途说明:修改系统组织框架 + * 参数说明 + * sysOrganization 系统组织框架对象 + * 返回值说明: 是否修改成功 + ***********************************/ + @Log(module = "系统组织框架", value = "修改企业或者部门信息!") + @PostMapping("/updateById") + @Operation(summary = "修改系统组织框架") + @ResponseBody + public ResponseResult updateById(@RequestBody SysOrganization sysOrganization) { + //填写 当前用户名称 + sysOrganization.setLastmodifier(userService.getUsername()); + //填写 当前日期 + sysOrganization.setLastmodifydate(new Timestamp(System.currentTimeMillis())); + //根据id 修改系统组织 + boolean isOk = organizationService.updateById(sysOrganization); + if (isOk) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /*********************************** + * 用途说明:修改系统组织框架 + * 参数说明 + * sysOrganization 系统组织框架对象 + * 返回值说明: 是否修改成功 + ***********************************/ + @Log(module = "系统组织框架", value = "设置企业/部门是否有效!") + @PostMapping("/setIsValid") + @Operation(summary = "设置组织是否有效") + @ResponseBody + public ResponseResult setIsValid(@RequestParam String id, + @RequestParam String isvaild) { + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + //根据id 修改是否有效,最近修改人,最近修改时间 + updateWrapper.eq("id", id).set("isvaild", isvaild).set("lastmodifier" + , userService.getUsername()).set("lastmodifydate", + new Timestamp(System.currentTimeMillis())); + boolean isOk = organizationService.update(updateWrapper); + if (isOk) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /*********************************** + * 用途说明:根据id删除系统组织框架 + * 参数说明 + * id 系统组织框架id + * 返回值说明: 是否删除成功 + ***********************************/ + @Log(module = "系统组织框架", value = "根据ID删除企业或者部门!") + @PostMapping("/deleteById") + @Operation(summary = "根据id删除系统组织框架") + @ResponseBody + public ResponseResult deleteById(@RequestParam String id) { + String[] orgIds = id.split(","); + for (String orgId : orgIds) { + LambdaQueryWrapper queryWrapper = + new LambdaQueryWrapper<>(); + List list = + organizationService.list(queryWrapper.eq(SysOrganization::getParentid, orgId)); + List ids = + list.stream().map(SysOrganization::getId).collect(Collectors.toList()); + boolean isOk = organizationService.removeById(orgId); + if (!isOk) { + continue; + } + for (String oid : ids) { + organizationService.removeById(oid); + } + } + return ResponseResult.success(); + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysRoleController.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysRoleController.java new file mode 100644 index 0000000..8267556 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/SysRoleController.java @@ -0,0 +1,318 @@ +package com.yfd.platform.system.system.controller; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.yfd.platform.annotation.Log; +import com.yfd.platform.config.ResponseResult; +import com.yfd.platform.system.domain.SysRole; +import com.yfd.platform.system.service.ISysRoleService; +import com.yfd.platform.system.service.IUserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.*; + +import jakarta.annotation.Resource; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + *

+ * 系统角色 前端控制器 + *

+ * + * @author zhengsl + * @since 2021-12-15 + */ +@RestController +@RequestMapping("/system/role") +@Tag(name = "系统角色") +public class SysRoleController { + + @Resource + private ISysRoleService roleService; + + @Resource + private IUserService userService; + + /*********************************** + * 用途说明:查询所有角色 + * 参数说明 + * roleName 角色名称 + * 返回值说明: 查询都有角色 + ***********************************/ + @PostMapping("/list") + @Operation(summary = "查询所有角色") + @ResponseBody + public List list(@RequestParam(required = false) String rolename) { + return roleService.selectRoleList(rolename); + } + + /*********************************** + * 用途说明:根据Id获取当个角色 + * 参数说明 + * id 角色表id + * 返回值说明: 根据id查询到角色详情 + ***********************************/ + @PostMapping("/getOneById") + @Operation(summary = "根据Id获取当个角色") + @ResponseBody + public ResponseResult getOneById(String id) { + SysRole sysRole = roleService.getById(id); + return ResponseResult.successData(sysRole); + } + + /*********************************** + * 用途说明:新增角色 + * 参数说明 + * sysRole 新增角色信息 + * 返回值说明: 是否新增成功 + ***********************************/ + @Log(module = "系统角色", value = "新增角色") + @PostMapping("/addRole") + @Operation(summary = "新增角色") + @ResponseBody + public ResponseResult addRole(@RequestBody SysRole sysRole) { + boolean isOk = roleService.addRole(sysRole); + if (isOk) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /*********************************** + * 用途说明:分配操作权限 + * 参数说明 + * id 角色id + * optscope 分配的权限 + * 返回值说明: 是否新增成功 + ***********************************/ + @Log(module = "系统角色", value = "分配操作权限") + @PostMapping("/setOptScope") + @Operation(summary = "分配操作权限") + @ResponseBody + public ResponseResult setOptScope(@RequestParam String id, + @RequestParam String optscope) { + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + //根据id 更新权限,最近修改人,最近修改时间 + updateWrapper.eq("id", id).set("optscope", optscope).set( + "lastmodifier", userService.getUsername()).set( + "lastmodifydate", LocalDateTime.now()); + boolean ok = roleService.update(updateWrapper); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /*********************************** + * 用途说明:角色菜单权限 + * 参数说明 + * id 角色id + * menuIds 权限id字符串 + * 返回值说明: 是否分配成功 + ***********************************/ + @Log(module = "系统角色", value = "角色菜单权限") + @PostMapping("/setMenuById") + @Operation(summary = "角色菜单权限") + @ResponseBody + public ResponseResult setMenuById(String id, String menuIds) { + if (StrUtil.isBlank(id)) { + return ResponseResult.error("参数为空"); + } + if (StrUtil.isBlank(menuIds)) { + return ResponseResult.success(); + } + boolean ok = roleService.setMenuById(id, menuIds); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + + } + + /*********************************** + * 用途说明:设置组织范围 + * 参数说明 + * id 角色id + * orgscope 组织范围 + * 返回值说明: 是否新增成功 + ***********************************/ + @Log(module = "系统角色", value = "设置组织范围") + @PostMapping("/setOrgscope") + @Operation(summary = "设置组织范围") + @ResponseBody + public ResponseResult setOrgscope(@RequestParam String id, + @RequestParam String orgscope) { + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + //根据id 更新组织范围,最近修改人,最近修改时间 + updateWrapper.eq("id", id).set("orgscope", orgscope).set( + "lastmodifier", userService.getUsername()).set( + "lastmodifydate", LocalDateTime.now()); + boolean ok = roleService.update(updateWrapper); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /*********************************** + * 用途说明:设置业务范围 + * 参数说明 + * id 角色id + * busscope 业务范围 + * 返回值说明: 是否新增成功 + ***********************************/ + @Log(module = "系统角色", value = "设置业务范围") + @PostMapping("/setBusscope") + @Operation(summary = "设置业务范围") + @ResponseBody + public ResponseResult setBusscope(@RequestParam String id, + @RequestParam String busscope) { + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + //根据id 更新业务范围,最近修改人,最近修改时间 + updateWrapper.eq("id", id).set("busscope", busscope).set( + "lastmodifier", userService.getUsername()).set( + "lastmodifydate", LocalDateTime.now()); + boolean ok = roleService.update(updateWrapper); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /*********************************** + * 用途说明:角色添加用户 + * 参数说明 + * roleid 角色id + * userids 用户id组 + * 返回值说明: 是否新增成功 + ***********************************/ + @Log(module = "系统角色", value = "角色添加用户") + @PostMapping("/setRoleUsers") + @Operation(summary = "角色添加用户") + @ResponseBody + public ResponseResult setRoleUsers(String roleid, String userids) { + boolean isOk = true; + String[] temp = userids.split(","); + for (String userid : temp) { + isOk = isOk && userService.addUserRoles(roleid, userid); + } + if (isOk) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /*********************************** + * 用途说明:删除角色用户 + * 参数说明 + * roleid 角色id + * 返回值说明: 是否新增成功 + ***********************************/ + @PostMapping("/deleteRoleUser") + @Operation(summary = "删除角色用户") + @ResponseBody + public ResponseResult deleteRoleUsers(@RequestParam String roleid, + @RequestParam String userids) { + //根据角色id、用户id删除 + boolean ok = roleService.deleteRoleUsers(roleid, userids); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /*********************************** + * 用途说明:设置角色是否有效 + * 参数说明 + * id 角色id + *isvaild 是否有效(1 是 0 否 ) + * 返回值说明: 是否新增成功 + ***********************************/ + @PostMapping("/setIsvaild") + @Operation(summary = "设置角色是否有效") + @ResponseBody + public ResponseResult setIsvaild(String id, String isvaild) { + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + //根据id 更新业务范围,最近修改人,最近修改时间 + updateWrapper.eq("id", id).set("isvaild", isvaild).set("lastmodifier" + , userService.getUsername()).set("lastmodifydate", + LocalDateTime.now()); + boolean ok = roleService.update(updateWrapper); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /*********************************** + * 用途说明:更新角色信息 + * 参数说明 + *sysRole 角色对象 + * 返回值说明: 是否修改成功 + ***********************************/ + @PostMapping("/updateById") + @Operation(summary = "更新角色信息") + @ResponseBody + public ResponseResult updateById(@RequestBody SysRole sysRole) { + //更新最近修改人 + sysRole.setLastmodifier(userService.getUsername()); + //更新最近修改时间 + sysRole.setLastmodifydate(new Timestamp(System.currentTimeMillis())); + //根据id更新角色信息 + boolean ok = roleService.updateById(sysRole); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /*********************************** + * 用途说明:根据id删除角色 + * 参数说明 + *id 角色id + * 返回值说明: 是否删除成功 + ***********************************/ + @PostMapping("/deleteById") + @Operation(summary = "根据id删除角色") + @ResponseBody + public ResponseResult deleteById(@RequestParam String id) { + roleService.deleteById(id); + return ResponseResult.success(); + } + + /*********************************** + * 用途说明:查询已分配的用户 + * 参数说明 + *orgid 所属组织 + *username 用户名称 + *status 状态 + *level 角色级别 + * rolename 角色名称 + * isvaild 角色是否有效 + * 返回值说明: 系统用户角色数据集合 + ***********************************/ + @PostMapping("/listRoleUsers") + @Operation(summary = "查询已分配的用户") + @ResponseBody + public List listRoleUsers(String orgid, String username, + String status, String level, + String rolename, String isvaild) { + return roleService.listRoleUsers(orgid, username, status, level, + rolename, isvaild); + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/UserController.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/UserController.java new file mode 100644 index 0000000..2b0d268 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/controller/UserController.java @@ -0,0 +1,188 @@ +package com.yfd.platform.system.system.controller; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.yfd.platform.annotation.Log; +import com.yfd.platform.config.ResponseResult; +import com.yfd.platform.datasource.DataSource; +import com.yfd.platform.system.domain.SysUser; +import com.yfd.platform.system.service.IUserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.annotation.Resource; +import java.sql.Timestamp; +import java.util.Map; + +/** + *

+ * 用户信息 前端控制器 + *

+ * + * @author zhengsl + * @since 2022-09-20 + */ +@RestController +@RequestMapping("/system/user") +@Tag(name = "系统用户") +public class UserController { + + @Resource + private IUserService userService; + + @Log(module = "系统用户", value = "新增系统用户") + @PostMapping("/addUser") + @Operation(summary = "新增系统用户") + @ResponseBody + public ResponseResult addUser(@RequestBody SysUser user, String roleids) { + Map reslut = userService.addUser(user, roleids); + return ResponseResult.successData(reslut); + } + + @Log(module = "系统用户", value = "修改用户信息") + @PostMapping("/updateUser") + @Operation(summary = "修改用户信息") + @ResponseBody + public ResponseResult updateUser(@RequestBody SysUser user, + String roleids) { + if (StrUtil.isEmpty(user.getId())) { + return ResponseResult.error("没有用户ID"); + } + //填写 当前用户名称 + user.setLastmodifier(userService.getUsername()); + //填写 当前日期 + user.setLastmodifydate(new Timestamp(System.currentTimeMillis())); + Map reslut = userService.updateById(user, roleids); + return ResponseResult.successData(reslut); + } + + @GetMapping("/queryUsers") + @Operation(summary = "查询用户信息") + @ResponseBody + public ResponseResult queryUsers(String orgid, + String username, Page page) { + + Page mapPage = userService.queryUsers(orgid, + username, page); + return ResponseResult.successData(mapPage); + } + + /*********************************** + * 用途说明:用户分配角色 + * 参数说明 + *idMap 用户id与角色id + * 返回值说明: 判断是否添加成功 + ************************************/ + @Log(module = "系统用户", value = "用户分配角色") + @PostMapping("/setUserRoles") + @Operation(summary = "用户分配角色") + @ResponseBody + public ResponseResult setUserRoles(String roleid, String userids) { + boolean ok = userService.setUserRoles(roleid, userids); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /*********************************** + * 用途说明:根据id删除用户 + * 参数说明 + *id 用户id + * 返回值说明: 判断是否删除成功 + ************************************/ + @Log(module = "系统用户", value = "根据ID删除用户") + @PostMapping("/deleteById") + @Operation(summary = "根据ID删除用户") + @ResponseBody + public ResponseResult deleteById(String id) { + userService.deleteById(id); + return ResponseResult.success(); + } + + /*********************************** + * 用途说明:根据ID批量删除用户 + * 参数说明 + *ids 用户id集合 + * 返回值说明: 判断是否删除成功 + ************************************/ + @Log(module = "系统用户", value = "根据ID批量删除用户") + @PostMapping("/deleteUserByIds") + @Operation(summary = "根据ID批量删除用户") + @ResponseBody + public ResponseResult deleteUserByIds(String id) { + if (StrUtil.isBlank(id)) { + return ResponseResult.error("参数为空"); + } + boolean ok = userService.deleteUserByIds(id); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /*********************************** + * 用途说明:重置用户密码(管理员) + * 参数说明 + *id 重置密码的 用户id + * 返回值说明: 判断是否重置成功 + ************************************/ + @Log(module = "系统用户", value = "重置用户密码") + @PostMapping("/resetPassword") + @Operation(summary = "重置用户密码") + @ResponseBody + @DataSource + public ResponseResult resetPassword(String id) throws Exception { + if (StrUtil.isBlank(id)) { + ResponseResult.error("参数为空"); + } + boolean ok = userService.resetPassword(id); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /*********************************** + * 用途说明:设置账号状态(管理员) + * 参数说明 + *id 用户id + * status 设置状态 + * 返回值说明: 判断是否设置成功 + ************************************/ + @Log(module = "系统用户", value = "设置账号状态") + @PostMapping("/setStatus") + @Operation(summary = "设置账号状态") + @ResponseBody + public ResponseResult setStatus(@RequestParam String id, + @RequestParam String status) { + boolean ok = userService.setStatus(id, status); + if (ok) { + return ResponseResult.success(); + } else { + return ResponseResult.error(); + } + } + + /*********************************** + * 用途说明:修改头像(管理员) + * 参数说明 + * multipartFile 文件对象 + * status 设置状态 + * 返回值说明: 文件名 + ************************************/ + @Operation(summary = "修改头像") + @PostMapping(value = "/updateAvatar") + public ResponseResult updateAvatar(String id, MultipartFile multipartFile) { + if (multipartFile == null) { + ResponseResult.error("参数为空"); + } + boolean ok = userService.uploadAvatar(id, multipartFile); + return ResponseResult.success(); + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/Dictionary.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/Dictionary.java new file mode 100644 index 0000000..79d97fb --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/Dictionary.java @@ -0,0 +1,78 @@ +package com.yfd.platform.system.system.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +/** + *

+ * 数据字典表 + *

+ * + * @author zhengsl + * @since 2021-10-27 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("RCA_DICTIONARY") +public class Dictionary implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + @TableId(type = IdType.ASSIGN_UUID) + private String id; + + /** + * 类型 + */ + private String type; + + /** + * 类型名称 + */ + private String typename; + + /** + * 代码 + */ + private String code; + + /** + * 名称 + */ + private String name; + + /** + * 顺序号 + */ + private String orderno; + + /** + * 上级代码 + */ + private String parentcode; + + /** + * 备用1 + */ + private String custom1; + + /** + * 备用2 + */ + private String custom2; + + /** + * 备用3 + */ + private String custom3; + + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/LoginUser.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/LoginUser.java new file mode 100644 index 0000000..02ee6c9 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/LoginUser.java @@ -0,0 +1,76 @@ +package com.yfd.platform.system.system.domain; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class LoginUser implements UserDetails { + + private SysUser user; + + private List permissions; + + public LoginUser(SysUser user, List permissions) { + this.user = user; + this.permissions = permissions; + } + + @JSONField(serialize = false) + private List authorities; + + @Override + public Collection getAuthorities() { + // 将权限信息放入集合 + authorities = permissions.stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + return authorities; + } + + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public String getUsername() { + return user.getUsername(); + } + + //获取用户昵称 + public String geNickname() { + return user.getNickname(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/Message.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/Message.java new file mode 100644 index 0000000..c6094b5 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/Message.java @@ -0,0 +1,117 @@ +package com.yfd.platform.system.system.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + *

+ * 消息通知 + *

+ * + * @author TangWei + * @since 2023-03-19 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("SYS_MESSAGE") +public class Message implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @TableId(type = IdType.ASSIGN_UUID) + @Schema(description = "ID") + private String id; + + /** + * 创建时间:排序 + */ + @Schema(description = "创建时间:排序") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Timestamp createtime; + + /** + * 消息类型:1-定时任务 2-工作流触发 3-人工触发 + */ + @Schema(description = "消息类型:1-定时任务 2-工作流触发 3-人工触发") + private String type; + + /** + * 消息标题 + */ + @Schema(description = "消息标题") + private String title; + + /** + * 消息内容 + */ + @Schema(description = "消息内容") + private String content; + + /** + * 发送者名称,定时器,人员 + */ + @Schema(description = "发送者名称,定时器,人员") + private String senderName; + + /** + * 接收者代码 人员账号列表 + */ + @Schema(description = "接收者代码 人员账号列表 ") + private String receiverCodes; + + /** + * 接收者名称:为空 即为所有人,人员名称列表 + */ + @Schema(description = "接收者名称:为空 即为所有人,人员名称列表") + private String receiverNames; + + /** + * 状态:1、初始创建 2-消息已阅 9-消息过期 + */ + @Schema(description = "状态:1、初始创建 2-消息已阅 9-消息过期") + private String status; + + /** + * 有效期:小时 + */ + @Schema(description = "有效期:小时") + private Integer validperiod; + + /** + * 已阅时间 + */ + @Schema(description = "已阅时间") + private Timestamp readtime; + + /** + * 备用1 + */ + @Schema(description = "备用1") + private String custom1; + + /** + * 备用2 + */ + @Schema(description = "备用2") + private String custom2; + + /** + * 备用3 + */ + @Schema(description = "备用3") + private String custom3; + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/QuartzJob.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/QuartzJob.java new file mode 100644 index 0000000..efa8c67 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/QuartzJob.java @@ -0,0 +1,118 @@ +package com.yfd.platform.system.system.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + *

+ * 定时任务 + *

+ * + * @author TangWei + * @since 2023-03-19 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("SYS_QUARTZ_JOB") +public class QuartzJob implements Serializable { + + public static final String JOB_KEY = "JOB_KEY"; + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(description = "ID") + @TableId(type = IdType.ASSIGN_UUID) + private String id; + + /** + * 排序号 + */ + @Schema(description = "排序号") + private Integer orderno; + + /** + * 任务名称 + */ + @Schema(description = "任务名称") + private String jobName; + + /** + * 执行类名称 + */ + @Schema(description = "执行类名称") + private String jobClass; + + /** + * 执行方法名称 + */ + @Schema(description = "执行方法名称") + private String jobMethod; + + /** + * 时间周期表达式 + */ + @Schema(description = "时间周期表达式") + private String jobCron; + + /** + * 方法参数 + */ + @Schema(description = "方法参数") + private String jobParams; + + /** + * 任务描述 + */ + @Schema(description = "任务描述") + private String description; + + /** + * 状态:0-暂停、1-启用 + */ + @Schema(description = "状态:0-暂停、1-启用") + private String status; + + /** + * 最近修改者 + */ + @Schema(description = "最近修改者") + private String lastmodifier; + + /** + * 最近修改日期 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + @Schema(description = "最近修改日期") + private Timestamp lastmodifydate; + + /** + * 备用1 + */ + @Schema(description = "备用1") + private String custom1; + + /** + * 备用2 + */ + @Schema(description = "备用2") + private String custom2; + + /** + * 备用3 + */ + @Schema(description = "备用3") + private String custom3; + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysConfig.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysConfig.java new file mode 100644 index 0000000..b45ba45 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysConfig.java @@ -0,0 +1,78 @@ +package com.yfd.platform.system.system.domain; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + *

+ * 系统全局配置 + *

+ * + * @author zhengsl + * @since 2022-01-19 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("SYS_CONFIG") +public class SysConfig implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + @TableId(type = IdType.ASSIGN_UUID) + private String id; + + /** + * 欢迎词 + */ + private String welcome; + + /** + * 系统功能介绍 + */ + private String funcation; + + /** + * 系统版本信息 + */ + private String versioninfo; + + /** + * 备注 + */ + private String remark; + + /** + * 最近修改者 + */ + private String lastmodifier; + + /** + * 最近修改日期 + */ + @TableField(fill = FieldFill.UPDATE) + private Timestamp lastmodifydate; + + /** + * 备用1 + */ + private String custom1; + + /** + * 备用2 + */ + private String custom2; + + /** + * 备用3 + */ + private String custom3; + + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysDictionary.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysDictionary.java new file mode 100644 index 0000000..901912d --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysDictionary.java @@ -0,0 +1,73 @@ +package com.yfd.platform.system.system.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +/** + *

+ * 数据字典表 + *

+ * + * @author TangWei + * @since 2023-03-08 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("SYS_DICTIONARY") +public class SysDictionary implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + @TableId(type = IdType.ASSIGN_UUID) + private String id; + + /** + * 字典类型 00-系统内置 01-用户配置 + */ + @TableField("DICTTYPE") + private String dictType; + + /** + * 顺序号 + */ + @TableField("ORDERNO") + private Integer orderNo; + + /** + * 字典编码 + */ + @TableField("DICTCODE") + private String dictCode; + + /** + * 字典名称 + */ + @TableField("DICTNAME") + private String dictName; + + /** + * 备用1 + */ + private String custom1; + + /** + * 备用2 + */ + private String custom2; + + /** + * 备用3 + */ + private String custom3; + + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysDictionaryItems.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysDictionaryItems.java new file mode 100644 index 0000000..86b2dcf --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysDictionaryItems.java @@ -0,0 +1,79 @@ +package com.yfd.platform.system.system.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +/** + *

+ * 数据字典明细 + *

+ * + * @author TangWei + * @since 2023-03-08 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("SYS_DICTIONARY_ITEMS") +public class SysDictionaryItems implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + @TableId(type = IdType.ASSIGN_UUID) + private String id; + + /** + * 对应字典ID + */ + @TableField("DICTID") + private String dictId; + + /** + * 顺序号 + */ + @TableField("ORDERNO") + private Integer orderNo; + + /** + * 项编码 + */ + @TableField("ITEMCODE") + private String itemCode; + + /** + * 项名称 + */ + @TableField("DICTNAME") + private String dictName; + + /** + * 父项编码 + */ + @TableField("PARENTCODE") + private String parentCode; + + /** + * 备用1 + */ + private String custom1; + + /** + * 备用2 + */ + private String custom2; + + /** + * 备用3 + */ + private String custom3; + + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysLog.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysLog.java new file mode 100644 index 0000000..7df5bb3 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysLog.java @@ -0,0 +1,94 @@ +package com.yfd.platform.system.system.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.sql.Timestamp; +import java.time.LocalDateTime; + +/** + *

+ * 系统操作日志 + *

+ * + * @author TangWei + * @since 2023-03-08 + */ +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = false) +@TableName("SYS_LOG") +public class SysLog implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @TableId(value = "ID", type = IdType.ASSIGN_UUID) + private String id; + + /** + * 用户账号 + */ + @TableField("USERCODE") + private String usercode; + + /** + * 用户名称 + */ + private String username; + + /** + * 操作类型 00-登录 01-新增 02-修改 03-删除 06-查询 09其他 + */ + @TableField("OPTTYPE") + private String opttype; + + /** + * 模块名称 + */ + private String module; + + /** + * 日志描述 + */ + private String description; + + /** + * 操作方法 + */ + private String method; + + /** + * 方法参数 + */ + private String params; + + /** + * 创建时间 + */ + @TableField("LOGTIME") + private Timestamp logtime; + + /** + * 请求IP + */ + @TableField("REQUESTIP") + private String requestip; + + /** + * 浏览器类型 + */ + private String browser; + + public SysLog(String opttype) { + this.opttype = opttype; + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysMenu.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysMenu.java new file mode 100644 index 0000000..d13bbdf --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysMenu.java @@ -0,0 +1,114 @@ +package com.yfd.platform.system.system.domain; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; + +/** + *

+ * 菜单及按钮 + *

+ * + * @author zhengsl + * @since 2021-12-15 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("SYS_MENU") +public class SysMenu implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + @TableId(type = IdType.ASSIGN_UUID) + private String id; + + /** + * 1-web 2-pad 3-mobile + */ + private String systemcode; + + /** + * 1-菜单 2-按钮 + */ + private String type; + + /** + * 在系统内自动生成 + */ + private String code; + + /** + * 名称 + */ + private String name; + + /** + * 图标地址 + */ + private String icon; + + /** + * 是否外链 + */ + private String islink; + + /** + * 内部模块路径或者外链地址 + */ + private String opturl; + + /** + * 权限控制标识 + */ + private String permission; + + /** + * 顶级为0 + */ + private String parentid; + + /** + * 排序号 + */ + private Integer orderno; + + /** + * 0-不显示 1-显示 + */ + private String isdisplay; + + /** + * 最近修改者 + */ + private String lastmodifier; + + /** + * 最近修改日期 + */ + @TableField(fill = FieldFill.UPDATE) + private Timestamp lastmodifydate; + + /** + * 备用1 + */ + private String custom1; + + /** + * 备用2 + */ + private String custom2; + + /** + * 备用3 + */ + private String custom3; + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysOrganization.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysOrganization.java new file mode 100644 index 0000000..081720f --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysOrganization.java @@ -0,0 +1,93 @@ +package com.yfd.platform.system.system.domain; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + *

+ * 系统组织框架 + *

+ * + * @author zhengsl + * @since 2021-12-15 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("SYS_ORGANIZATION") +public class SysOrganization implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + @TableId(type = IdType.ASSIGN_UUID) + private String id; + + /** + * 1-公司 -2-部门 + */ + private String orgtype; + + /** + * 两位一级 + */ + private String orgcode; + + /** + * 组织名称 + */ + private String orgname; + + /** + * 上级id + */ + private String parentid; + + /** + * 组织负责人 + */ + private String manager; + + /** + * 1-是 0-否 + */ + private String isvaild; + + /** + * 描述 + */ + private String description; + + /** + * 最近修改者 + */ + private String lastmodifier; + + /** + * 最近修改日期 + */ + @TableField(fill = FieldFill.UPDATE) + private Timestamp lastmodifydate; + + /** + * 备用1 + */ + private String custom1; + + /** + * 备用2 + */ + private String custom2; + + /** + * 备用3 + */ + private String custom3; + + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysRole.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysRole.java new file mode 100644 index 0000000..6ef0d8b --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysRole.java @@ -0,0 +1,99 @@ +package com.yfd.platform.system.system.domain; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.sql.Timestamp; + +/** + *

+ * 系统角色 + *

+ * + * @author zhengsl + * @since 2021-12-15 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("SYS_ROLE") +public class SysRole implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + @TableId(type = IdType.ASSIGN_UUID) + private String id; + + /** + * 系统生成,三位编号 + */ + private String rolecode; + + /** + * 角色名称 + */ + private String rolename; + + /** + * 1-超级管理员 2-单位管理员 3-普通用户 + */ + @TableField("\"LEVEL\"") + private String level; + + /** + * 描述 + */ + private String description; + + /** + * org1,org2 + */ + private String orgscope; + + /** + * 多个操作代码(菜单、按钮) + */ + private String optscope; + + /** + * json格式自定义业务范围 + */ + private String busscope; + + /** + * 1-是 0-否 + */ + private String isvaild; + + /** + * 最近修改者 + */ + private String lastmodifier; + + /** + * 最近修改日期 + */ + @TableField(fill = FieldFill.UPDATE) + private Timestamp lastmodifydate; + + /** + * 备用1 + */ + private String custom1; + + /** + * 备用2 + */ + private String custom2; + + /** + * 备用3 + */ + private String custom3; + + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysUser.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysUser.java new file mode 100644 index 0000000..9470f6e --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/domain/SysUser.java @@ -0,0 +1,120 @@ +package com.yfd.platform.system.system.domain; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.sql.Timestamp; +import java.util.List; + +/** + *

+ * 系统用户 + *

+ * + * @author zhengsl + * @since 2021-10-27 + */ + +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("SYS_USER") +public class SysUser implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id 主键 + */ + @TableId(type = IdType.ASSIGN_UUID) + private String id; + + /** + * 用户类型 0-管理员 1-普通用户 + */ + private Integer usertype; + + /** + * 用户名(账号) + */ + private String username; + /** + * 用户昵称 + */ + private String nickname; + + /** + * 登录密码(加密存储) + */ + private String password; + + /** + * 性别(0-男 1-女 ) + */ + private String sex; + + /** + * 邮箱 + */ + private String email; + /** + * 手机号 + */ + private String phone; + + /** + * 头像(预留) + */ + private String avatar; + + /** + * 账号状态(1-正常 0-停用) + */ + private Integer status; + + /** + * 部门ID + */ + private String orgid; + + /** + * 密码重置时间 + */ + private String pwdresettime; + + /** + * 最近修改者 + */ + private String lastmodifier; + + /** + * 最近修改日期 + */ + @TableField(fill = FieldFill.UPDATE) + private Timestamp lastmodifydate; + + @TableField(exist = false) + private String uuid; + + @TableField(exist = false) + private String code; + + /** + * 备用1 + */ + private String custom1; + + /** + * 备用2 + */ + private String custom2; + + /** + * 备用3 + */ + private String custom3; + + @TableField(exist = false) + List roles; +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/MessageMapper.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/MessageMapper.java new file mode 100644 index 0000000..98042e9 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/MessageMapper.java @@ -0,0 +1,16 @@ +package com.yfd.platform.system.system.mapper; + +import com.yfd.platform.system.domain.Message; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * 消息通知 Mapper 接口 + *

+ * + * @author TangWei + * @since 2023-03-19 + */ +public interface MessageMapper extends BaseMapper { + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/QuartzJobMapper.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/QuartzJobMapper.java new file mode 100644 index 0000000..3acb9ad --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/QuartzJobMapper.java @@ -0,0 +1,16 @@ +package com.yfd.platform.system.system.mapper; + +import com.yfd.platform.system.domain.QuartzJob; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * 定时任务 Mapper 接口 + *

+ * + * @author TangWei + * @since 2023-03-19 + */ +public interface QuartzJobMapper extends BaseMapper { + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysConfigMapper.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysConfigMapper.java new file mode 100644 index 0000000..78bfbf3 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysConfigMapper.java @@ -0,0 +1,17 @@ +package com.yfd.platform.system.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.yfd.platform.system.domain.SysConfig; + + +/** + *

+ * 系统全局配置 Mapper 接口 + *

+ * + * @author zhengsl + * @since 2022-01-19 + */ +public interface SysConfigMapper extends BaseMapper { + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysDictionaryItemsMapper.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysDictionaryItemsMapper.java new file mode 100644 index 0000000..a2dd3fd --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysDictionaryItemsMapper.java @@ -0,0 +1,17 @@ +package com.yfd.platform.system.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.yfd.platform.system.domain.SysDictionaryItems; + +/** + *

+ * 数据字典明细 Mapper 接口 + *

+ * + * @author TangWei + * @since 2023-03-08 + */ +public interface SysDictionaryItemsMapper extends BaseMapper { + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysDictionaryMapper.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysDictionaryMapper.java new file mode 100644 index 0000000..1a6659b --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysDictionaryMapper.java @@ -0,0 +1,22 @@ +package com.yfd.platform.system.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.yfd.platform.system.domain.SysDictionary; + +/** + *

+ * 数据字典表 Mapper 接口 + *

+ * + * @author TangWei + * @since 2023-03-08 + */ +public interface SysDictionaryMapper extends BaseMapper { + + /********************************** + * 用途说明: 根据字典类型获取字典最大序号 + * 参数说明 sysDictionary 字典对象 + * 返回值说明: 返回增加成功或者失败 + ***********************************/ + Integer selectMaxNo(String dictType); +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysLogMapper.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysLogMapper.java new file mode 100644 index 0000000..bea2ce1 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysLogMapper.java @@ -0,0 +1,16 @@ +package com.yfd.platform.system.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.yfd.platform.system.domain.SysLog; + +/** + *

+ * 系统操作日志 Mapper 接口 + *

+ * + * @author TangWei + * @since 2023-03-08 + */ +public interface SysLogMapper extends BaseMapper { + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysMenuMapper.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysMenuMapper.java new file mode 100644 index 0000000..390edb9 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysMenuMapper.java @@ -0,0 +1,59 @@ +package com.yfd.platform.system.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.yfd.platform.system.domain.SysMenu; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + *

+ * 菜单及按钮 Mapper 接口 + *

+ * + * @author zhengsl + * @since 2021-12-15 + */ +public interface SysMenuMapper extends BaseMapper { + + /*********************************** + * 用途说明:菜单及按钮序号向上移动 + * 参数说明 + * parentid 上级id + *Orderno 小于序号(原序号) + *upOrderno 大于等于序号(更改的序号加一) + * 返回值说明: 是否更新成功 + ***********************************/ + boolean upMoveOrderno(@Param("parentid") String parentid, @Param("Orderno") int Orderno, @Param("upOrderno") int upOrderno); + + /*********************************** + * 用途说明:菜单及按钮序号向下移动 + * 参数说明 + * parentid 上级id + *Orderno 大于序号(原序号) + *downOrderno 小于等于序号(更改的序号减一) + * 返回值说明: 是否更新成功 + ***********************************/ + boolean downMoveOrderno(@Param("parentid") String parentid, @Param("Orderno") int Orderno, @Param("downOrderno") int downOrderno); + + + List selectPermsByUserId(String userId); + + //List selectMenuByUserId(String userId); + List selectMenuByUserId(String userId); + + /*********************************** + * 用途说明:根据权限id查找系统类型 + * 参数说明 id 权限id + * 返回值说明: 返回系统类型 + ***********************************/ + String getSystemCodeById(String id); + + /*********************************** + * 用途说明:根据角色Id查找权限 + * 参数说明 id 权限id + * 返回值说明: 返回权限集合 + ***********************************/ + List selectMenuByRoleId(String id); +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysOrganizationMapper.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysOrganizationMapper.java new file mode 100644 index 0000000..eb37288 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysOrganizationMapper.java @@ -0,0 +1,33 @@ +package com.yfd.platform.system.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.yfd.platform.system.domain.SysOrganization; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + *

+ * 系统组织框架 Mapper 接口 + *

+ * + * @author zhengsl + * @since 2021-12-15 + */ +public interface SysOrganizationMapper extends BaseMapper { + + /*********************************** + * 用途说明:去重查询组织分类 + * 返回值说明: 所有组织分类 + ***********************************/ + List queryOrgtype(); + + /*********************************** + * 用途说明:根据组织分类查询上级id + * 参数说明 + * orgtype 组织分类 + * 返回值说明: 上级id + ***********************************/ + List queryParentid(@Param("orgtype") String orgtype); + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysRoleMapper.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysRoleMapper.java new file mode 100644 index 0000000..73926d2 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysRoleMapper.java @@ -0,0 +1,112 @@ +package com.yfd.platform.system.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.yfd.platform.system.domain.SysRole; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + *

+ * 系统角色 Mapper 接口 + *

+ * + * @author zhengsl + * @since 2021-12-15 + */ +public interface SysRoleMapper extends BaseMapper { + + /*********************************** + * 用途说明:根据角色id查询是否存在用户 + * 参数说明 + * roleid 角色id + * 返回值说明: 该角色下是否存在用户 + ************************************/ + List> isRoleUsersByroleid(String roleid); + + /*********************************** + * 用途说明:根据角色id查询是否存在权限 + * 参数说明 + * roleid 角色id + * 返回值说明: 该角色下是否存在权限 + ************************************/ + List> isRoleMenuByRoleId(String roleId); + + /*********************************** + * 用途说明:查询已分配的用户 + * 参数说明 + *orgid 所属组织 + *username 用户名称 + *status 状态 + *level 角色级别 '1-超级管理员 2-单位管理员 3-普通用户' + * rolename 角色名称 + * isvaild 角色是否有效 + * 返回值说明: 系统用户角色数据集合 + ***********************************/ + List listRoleUsers(String orgid, String username, String status, + String level, String rolename, String isvaild); + + /*********************************** + * 用途说明:根据 角色id和用户id 删除 (admin除外) + * 参数说明 + *roleid 角色id + * urserid 用户id + * 返回值说明: 是否删除成功 + ***********************************/ + boolean deleteRoleUsers(String roleid, String urserid); + + /********************************** + * 用途说明: 根据用户id获取角色信息 + * 参数说明 id 角色id + * 返回值说明: void + ***********************************/ + List getRoleByUserId(String id); + + /********************************** + * 用途说明: 根据角色ID删除菜单与角色关联信息 + * 参数说明 id 角色id + * 返回值说明: void + ***********************************/ + boolean deleteRoleMenus(String id); + + /********************************** + * 用途说明: 根据角色ID删除用户与角色关联信息 + * 参数说明 id 角色id + * 返回值说明: void + ***********************************/ + boolean deleteRoleUser(String id); + + /********************************** + * 用途说明: 根据角色id获取用户id + * 参数说明 id 角色id + * 返回值说明: 用户id + ***********************************/ + List getUserIdById(String id); + + void addRoleMenu(@Param("id") String id, @Param("roleid") String roleid, + @Param("menuid") String menuid); + + + /********************************** + * 用途说明:根据用户 id 获取角色信息 + * 参数说明 id 用户 id + * 返回值说明:角色列表 + ***********************************/ + List getRoleListByUserId(String id); + + /********************************** + * 用途说明:根据用户 id 获取角色 ID 列表 + * 参数说明 id 用户 id + * 返回值说明:角色 ID 列表 + ***********************************/ + List getRoleIdsByUserId(String id); + + /********************************** + * 用途说明:查询角色列表(Oracle 兼容) + * 参数说明:rolename - 角色名称 + * 返回值说明:角色列表 + ***********************************/ + List selectRoleList(@Param("rolename") String rolename); + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysUserMapper.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysUserMapper.java new file mode 100644 index 0000000..243d178 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/mapper/SysUserMapper.java @@ -0,0 +1,96 @@ +package com.yfd.platform.system.system.mapper; + + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.yfd.platform.system.domain.SysUser; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + *

+ * 系统用户表 Mapper 接口 + *

+ * + * @author zhengsl + * @since 2021-10-27 + */ +public interface SysUserMapper extends BaseMapper { + List list(@Param("total")String total, @Param("size")String size, @Param("orgid")String orgid, @Param("username")String username, @Param("mobile")String mobile , @Param("status")String status); + + /*********************************** + * 用途说明:新增系统角色用户对照表 对用户分配角色 + * 参数说明 + * id 生成的id + * roleid 角色id + * userid 用户id + * 返回值说明: + ************************************/ + boolean addUserRoles(@Param("id")String id,@Param("roleid") String roleid,@Param("userid") String userid); + + /*********************************** + * 用途说明:根据用户id 和角色id 查询 系统角色用户对照表 + * 参数说明 + * userid 用户id + * roleid 角色id + * 返回值说明: + ************************************/ + List getRoleUsersByid(@Param("roleid") String roleid,@Param("userid") String userid); + + /*********************************** + * 用途说明:根据用户表id查询角色表所有角色 + * 参数说明 + * userid 用户id + * 返回值说明: + ************************************/ + List getLevel(@Param("userid") String userid); + + /*********************************** + * 用途说明:根据用户表id查询角色表所有角色id + * 参数说明 + * userid 用户id + * 返回值说明: + ************************************/ + List getRoleid(@Param("userid") String userid); + + /*********************************** + * 用途说明:根据用户表id查询角色表级别 + * 参数说明 + * userid 用户id + * 返回值说明: + ************************************/ + String getMaxLevel(@Param("userid") String userid); + + /*********************************** + * 用途说明:根据用户id删除所分配的角色 + * 参数说明 + * userid 用户id + * 返回值说明: + ************************************/ + boolean delRoleUsersByUserid(@Param("userid") String userid); + + /*********************************** + * 用途说明:根据用户id删除所分配的不包含角色 + * 参数说明 + * userid 用户id + * roleids 多个角色id + * 返回值说明: + ************************************/ + boolean delInRoleUsersByUserid(@Param("userid") String userid,@Param("roleids")String[] roleids); + + Page queryUsers(String orgid, + String username, + Page page); + + Map getOrganizationByid(String id); + + /********************************** + * 用途说明: 根据ID删除用户与角色的关联信息 + * 参数说明 ids 用户id集合 + * 返回值说明: void + ***********************************/ + void delRoleUsersByUserIds(List ids); +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/IMessageService.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/IMessageService.java new file mode 100644 index 0000000..2b17e23 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/IMessageService.java @@ -0,0 +1,16 @@ +package com.yfd.platform.system.system.service; + +import com.yfd.platform.system.domain.Message; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * 消息通知 服务类 + *

+ * + * @author TangWei + * @since 2023-03-19 + */ +public interface IMessageService extends IService { + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/IQuartzJobService.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/IQuartzJobService.java new file mode 100644 index 0000000..ab493bc --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/IQuartzJobService.java @@ -0,0 +1,43 @@ +package com.yfd.platform.system.system.service; + +import com.yfd.platform.system.domain.QuartzJob; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * 定时任务 服务类 + *

+ * + * @author TangWei + * @since 2023-03-19 + */ +public interface IQuartzJobService extends IService { + + /********************************** + * 用途说明: 新增定时任务 + * 参数说明 quartzJob 定时对象 + * 返回值说明: boolean 是否成功 + ***********************************/ + boolean addQuartzJob(QuartzJob quartzJob); + + /********************************** + * 用途说明: 删除定时任务 + * 参数说明 id id + * 返回值说明: boolean 是否成功 + ***********************************/ + boolean deleteQuartzJob(String id); + + /********************************** + * 用途说明: 拖动修改定时任务顺序 + * 参数说明 fromID 当前ID toID 到达ID + * 返回值说明: com.yfd.platform.config.ResponseResult 返回拖动成功或者失败 + ***********************************/ + boolean changeDictOrder(String fromID, String toID); + + /********************************** + * 用途说明: 执行定时任务 + * 参数说明 id id + * 返回值说明: void + ***********************************/ + void execution(QuartzJob byId); +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysConfigService.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysConfigService.java new file mode 100644 index 0000000..b1b6ffc --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysConfigService.java @@ -0,0 +1,22 @@ +package com.yfd.platform.system.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.yfd.platform.system.domain.SysConfig; + +import javax.sound.sampled.UnsupportedAudioFileException; +import java.io.IOException; +import java.util.Map; + +/** + *

+ * 系统全局配置 服务类 + *

+ * + * @author zhengsl + * @since 2022-01-19 + */ +public interface ISysConfigService extends IService { + + + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysDictionaryItemsService.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysDictionaryItemsService.java new file mode 100644 index 0000000..6385a0e --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysDictionaryItemsService.java @@ -0,0 +1,47 @@ +package com.yfd.platform.system.system.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.yfd.platform.system.domain.SysDictionaryItems; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; + +/** + *

+ * 数据字典明细 服务类 + *

+ * + * @author TangWei + * @since 2023-03-08 + */ +public interface ISysDictionaryItemsService extends IService { + + /********************************** + * 用途说明: 分页查询字典项信息 + * 参数说明 dictID 字典ID ItemName 字典项名称 pageNum 当前页 + * 返回值说明: com.yfd.platform.config.ResponseResult 返回分页查询结果 + ***********************************/ + Page getDictItemPage(String dictId, String itemName, Page page); + + /********************************** + * 用途说明: 增加字典项 + * 参数说明 sysDictionaryItems 字典项信息 + * 返回值说明: com.yfd.platform.config.ResponseResult 返回增加成功或者失败 + ***********************************/ + boolean addDictionaryItem(SysDictionaryItems sysDictionaryItems); + + /********************************** + * 用途说明: 拖动修改字典项顺序 + * 参数说明 fromID 当前ID toID 到达ID + * 返回值说明: com.yfd.platform.config.ResponseResult 返回拖动成功或者失败 + ***********************************/ + boolean changeItemOrder(String fromID, String toID); + + /********************************** + * 用途说明: 导出数据字典项数据 + * 参数说明 sysDictionaryItemsList 所需导出的字典项集合 + * 返回值说明: com.yfd.platform.config.ResponseResult 返回导出成功或失败 + ***********************************/ + void exportExcel(List records, HttpServletResponse response); +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysDictionaryService.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysDictionaryService.java new file mode 100644 index 0000000..f3153c1 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysDictionaryService.java @@ -0,0 +1,45 @@ +package com.yfd.platform.system.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.yfd.platform.system.domain.SysDictionary; + +import java.util.List; + +/** + *

+ * 数据字典表 服务类 + *

+ * + * @author TangWei + * @since 2023-03-08 + */ +public interface ISysDictionaryService extends IService { + + /********************************** + * 用途说明: 获取数据字典列表 + * 参数说明 dictType 字典类型 + * 返回值说明: com.yfd.platform.config.ResponseResult 返回分页查询结果 + ***********************************/ + List getDictList(String dictType); + + /********************************** + * 用途说明: 新增字典 + * 参数说明 sysDictionary 字典对象 + * 返回值说明: com.yfd.platform.config.ResponseResult 返回增加成功或者失败 + ***********************************/ + boolean addDict(SysDictionary sysDictionary); + + /********************************** + * 用途说明: 根据ID删除字典 + * 参数说明 id 字典ID + * 返回值说明: com.yfd.platform.config.ResponseResult 返回删除结果成功或者失败 + ***********************************/ + boolean deleteDictById(String id); + + /********************************** + * 用途说明: 拖动修改字典顺序 + * 参数说明 fromID 当前ID toID 到达ID + * 返回值说明: com.yfd.platform.config.ResponseResult 返回拖动成功或者失败 + ***********************************/ + boolean changeDictOrder(String fromID, String toID); +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysLogService.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysLogService.java new file mode 100644 index 0000000..2128a62 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysLogService.java @@ -0,0 +1,52 @@ +package com.yfd.platform.system.system.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.yfd.platform.system.domain.SysLog; +import org.aspectj.lang.ProceedingJoinPoint; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + *

+ * 系统操作日志 服务类 + *

+ * + * @author TangWei + * @since 2023-03-08 + */ +public interface ISysLogService extends IService { + + /********************************** + * 用途说明: 分页查询日志信息 + * 参数说明 pageNum(页码数)、pageSize(页大小,如果是固定页大小可不传)、username(用户名)、(optType) + * 操作类型、startDate(开始日期)、endDate(结束日期) + * 返回值说明: com.yfd.platform.config.ResponseResult 返回分页查询结果 + ***********************************/ + Page getLogList(String username, String optType, + String startDate, + String endDate, Page page); + + + /********************************** + * 用途说明: 导出日志数据 + * 参数说明 sysLogs 所需导出的字典项集合 + * 返回值说明: com.yfd.platform.config.ResponseResult 返回导出成功或者失败 + ***********************************/ + void exportExcel(List sysLogs, HttpServletResponse response) throws IOException; + + /********************************** + * 用途说明: 新增日志 + * 参数说明 nickname 用户名 + * 参数说明 username 用户账号 + * 参数说明 browser 浏览器 + * 参数说明 ip 本机Ip地址 + * 参数说明 joinPoint 连接点 + * 参数说明 log 日志信息 + * 返回值说明: void + ***********************************/ + void save(String nickname,String username, String browser, String ip, ProceedingJoinPoint joinPoint, SysLog log); +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysMenuService.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysMenuService.java new file mode 100644 index 0000000..4d4b017 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysMenuService.java @@ -0,0 +1,101 @@ +package com.yfd.platform.system.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; + +import com.yfd.platform.system.domain.SysMenu; +import org.springframework.web.multipart.MultipartFile; + +import java.io.FileNotFoundException; +import java.util.List; +import java.util.Map; + +/** + *

+ * 菜单及按钮 服务类 + *

+ * + * @author zhengsl + * @since 2021-12-15 + */ +public interface ISysMenuService extends IService { + + /*********************************** + * 用途说明:获取菜单结构树(含按钮) + * 参数说明 + * systemcode 系统 + * name 名称 + * isdisplay 是否显示 + * 返回值说明: 菜单结构树集合 + ***********************************/ + List> getMenuButtonTree(String systemcode, String name, String isdisplay); + + /*********************************** + * 用途说明:获取菜单结构树(不含按钮) + * 参数说明 + * systemcode 系统 + * name 名称 + * isdisplay 是否显示 + * 返回值说明: 菜单结构树集合 + ***********************************/ + List> getMenuTree(String systemcode, String name, String isdisplay); + + + /*********************************** + * 用途说明:新增菜单及按钮 + * 参数说明 + * sysMenu 菜单或按钮表对象 + * 返回值说明: 是否添加成功提示 + ***********************************/ + boolean addMenu(SysMenu sysMenu); + + /*********************************** + * 用途说明:上传单个图标 + * 参数说明 + * id 上传图标id + * icon 图标 + * 返回值说明: 是否上传成功 + ***********************************/ + boolean uploadIcon(String id, MultipartFile icon); + + /*********************************** + * 用途说明:根据id删除单个图标 + * 参数说明 + * id 删除图标id + * icon 图标名称 + * 返回值说明: 是否删除成功 + ***********************************/ + boolean deleteIcon(String id); + + /*********************************** + * 用途说明:菜单及按钮序号排序 + * 参数说明 + * parentid 上级id + * orderMap map<菜单及按钮表id,排列序号> + * 返回值说明: 是否更新成功 + ***********************************/ + boolean moveOrderno(String parentid, String id, int orderno); + + /*********************************** + * 用途说明:根据id删除菜单或按钮 + * 参数说明 + * id 删除列的id + * 返回值说明: 是否删除成功 + ***********************************/ + boolean deleteById(String id); + + boolean changeOderNoById(String fromId, String toId); + + List> getMenuTree(String id); + + /*********************************** + * 用途说明:权限分配 + * 参数说明 + * systemcode 系统 + * name 名称 + * isdisplay 是否显示 + * 返回值说明: 菜单结构树集合 + ***********************************/ + List> permissionAssignment(String code,String roleId); + + String uploadIcon(MultipartFile icon) throws FileNotFoundException; +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysOrganizationService.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysOrganizationService.java new file mode 100644 index 0000000..545c2ba --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysOrganizationService.java @@ -0,0 +1,60 @@ +package com.yfd.platform.system.system.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import com.yfd.platform.system.domain.SysOrganization; + +import java.util.List; +import java.util.Map; + +/** + *

+ * 系统组织框架 服务类 + *

+ * + * @author zhengsl + * @since 2021-12-15 + */ +public interface ISysOrganizationService extends IService { + + /*********************************** + * 用途说明:获取组织结构树 + * 参数说明 + *parentid 上级id + * params 名称(根据名称查询二级) + * 返回值说明: 组织树集合 + ***********************************/ + List> getOrgTree(String parentid, String params); + + /*********************************** + * 用途说明:新增系统组织框架 + * 参数说明 + * sysOrganization 系统组织框架对象 + * 返回值说明: 是否新增成功 + ***********************************/ + boolean addOrg(SysOrganization sysOrganization); + + /*********************************** + * 用途说明:根据企业ID查询组织详情 + * 参数说明 + * id 企业id + * 返回值说明: 系统组织框架对象 + ***********************************/ + List getOrganizationById(String id,String orgName); + + /*********************************** + * 用途说明:获取组织范围树结构 + * 参数说明 + *roleId 角色id + * 返回值说明: 组织树集合 + ***********************************/ + List> getOrgScopeTree(String roleId); + + /********************************** + * 用途说明: 修改角色组织范围 + * 参数说明 roleId 角色id + * 参数说明 orgscope 组织id集合 + * 返回值说明: boolean 是否修改成功 + ***********************************/ + boolean updateOrgScopeByRoleId(String roleId, String orgscope); +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysRoleService.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysRoleService.java new file mode 100644 index 0000000..bf3da11 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/ISysRoleService.java @@ -0,0 +1,68 @@ +package com.yfd.platform.system.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.yfd.platform.system.domain.SysRole; + +import java.util.List; +import java.util.Map; + +/** + *

+ * 系统角色 服务类 + *

+ * + * @author zhengsl + * @since 2021-12-15 + */ +public interface ISysRoleService extends IService { + + /*********************************** + * 用途说明:新增角色 + * 参数说明 + * sysRole 新增角色信息 + * 返回值说明: 是否新增成功 + ***********************************/ + boolean addRole(SysRole sysRole); + + /*********************************** + * 用途说明:删除角色用户 + * 参数说明 + * id 系统角色用户对照表id + * 返回值说明: 是否新增成功 + ***********************************/ + + boolean deleteRoleUsers(String roleid, String urserids); + + /*********************************** + * 用途说明:根据id删除角色 + * 参数说明 + *id 角色id + * 返回值说明: 是否删除成功 + ***********************************/ + void deleteById(String id); + + /*********************************** + * 用途说明:查询已分配的用户 + * 参数说明 + *orgid 所属组织 + *username 用户名称 + *status 状态 + *level 角色级别 + * rolename 角色名称 + * isvaild 角色是否有效 + * 返回值说明: 系统用户角色数据集合 + ***********************************/ + List listRoleUsers(String orgid, String username, String status, String level, String rolename, String isvaild); + + + /*********************************** + * 用途说明:角色分配权限 + * 参数说明 + * id 角色id + * menuIds 权限id字符串 + * 返回值说明: 是否分配成功 + ***********************************/ + boolean setMenuById(String id, String menuIds); + + List selectRoleList(String rolename); +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/IUserService.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/IUserService.java new file mode 100644 index 0000000..577bb54 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/IUserService.java @@ -0,0 +1,143 @@ +package com.yfd.platform.system.system.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.yfd.platform.config.ResponseResult; +import com.yfd.platform.system.domain.LoginUser; +import com.yfd.platform.system.domain.SysUser; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.util.List; +import java.util.Map; + +/** + *

+ * 系统用户 + *

+ * + * @author zhengsl + * @since 2021-10-27 + */ +public interface IUserService extends IService { + + //获取当前用户账号及名称 + String getUsername(); + + //获取当前用户信息 + SysUser getUserInfo(); + /*********************************** + * 用途说明:获取当前用户账号与姓名 + * 返回值说明: 当前用户账号与姓名 + ************************************/ + Map getNameInfo(); + //获取当前用户信息带权限 + ResponseResult getLoginUserInfo(); + + /*********************************** + * 用途说明:新增用户 + * 参数说明 + *sysUser 新增用户对象 + * id 创建者id + * roleId 角色id + * 返回值说明: 提示字符串 + ************************************/ + Map addUser(SysUser sysUser, String roleids); + + /*********************************** + * 用途说明:查询系统用户 + * 参数说明 + *page 分页集合参数 + *orgid 所属组织 + *username 用户名称 + * mobile 手机号 + * status 状态 + * 返回值说明: 用户分页集合 + ************************************/ + List list(String total, String size, String orgid, String username, + String mobile, String status); + + /*********************************** + * 用途说明:根据ID查询用户详情 + * 参数说明 + *id 用户id + * 返回值说明: 用户表对象 + ************************************/ + Map getOneById(String id); + + /*********************************** + * 用途说明:根据ID修改用户 + * 参数说明 + *sysUser 用户对象 + *roleids 角色id + * 返回值说明: 是否更新成功 + ************************************/ + Map updateById(SysUser sysUser, String roleids); + + /*********************************** + * 用途说明:用户分配角色(多个) + * 参数说明 + *roleid 角色id + * userids 用户id数组 + * 返回值说明: 判断是否添加成功 + ************************************/ + boolean setUserRoles(String roleid, String userids); + + /*********************************** + * 用途说明:根据id删除用户 + * 参数说明 + *id 用户id + * 返回值说明: 判断是否删除成功 + ************************************/ + boolean deleteById(String id); + + /*********************************** + * 用途说明:重置用户密码(管理员) + * 参数说明 + *id 重置密码的 用户id + * 返回值说明: 判断是否重置成功 + ************************************/ + boolean resetPassword(String id) throws Exception; + + /*********************************** + * 用途说明:设置账号状态(管理员) + * 参数说明 + *id 用户id + * status 设置状态 + * 返回值说明: 判断是否设置成功 + ************************************/ + boolean setStatus(String id, String status); + + /*********************************** + * 用途说明:上传用户头像 + * 参数说明 + * id 用户id + * img 账号头像 + * 返回值说明: 判断是否上传 + ***********************************/ + boolean uploadAvatar(String id, MultipartFile img); + + /*********************************** + * 用途说明:新增系统角色用户对照表 对用户分配角色(单个) + * 参数说明 + * id 生成的id + * userid 用户id + * roleid 角色id + * 返回值说明: + ************************************/ + boolean addUserRoles(String roleid, String userid); + + //Page queryUsers(String orgid, String username, Page page); + Page queryUsers(String orgid, String username, Page page); + + /*********************************** + * 用途说明:根据ID批量删除用户 + * 参数说明 + *ids 用户id集合 + * 返回值说明: 判断是否删除成功 + ************************************/ + boolean deleteUserByIds(String ids); + + + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/MessageServiceImpl.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/MessageServiceImpl.java new file mode 100644 index 0000000..6f66644 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/MessageServiceImpl.java @@ -0,0 +1,20 @@ +package com.yfd.platform.system.system.service.impl; + +import com.yfd.platform.system.domain.Message; +import com.yfd.platform.system.mapper.MessageMapper; +import com.yfd.platform.system.service.IMessageService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Service; + +/** + *

+ * 消息通知 服务实现类 + *

+ * + * @author TangWei + * @since 2023-03-19 + */ +@Service +public class MessageServiceImpl extends ServiceImpl implements IMessageService { + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/QuartzJobServiceImpl.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/QuartzJobServiceImpl.java new file mode 100644 index 0000000..5198726 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/QuartzJobServiceImpl.java @@ -0,0 +1,114 @@ +package com.yfd.platform.system.system.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.yfd.platform.system.domain.QuartzJob; +import com.yfd.platform.system.domain.SysDictionary; +import com.yfd.platform.system.mapper.QuartzJobMapper; +import com.yfd.platform.system.service.IQuartzJobService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.yfd.platform.utils.QuartzManage; +import org.springframework.stereotype.Service; + +import jakarta.annotation.Resource; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + *

+ * 定时任务 服务实现类 + *

+ * + * @author TangWei + * @since 2023-03-19 + */ +@Service +public class QuartzJobServiceImpl extends ServiceImpl implements IQuartzJobService { + + @Resource + private QuartzJobMapper quartzJobMapper; + + @Resource + private QuartzManage quartzManage; + + /********************************** + * 用途说明: 新增定时任务 + * 参数说明 quartzJob 定时对象 + * 返回值说明: boolean 是否成功 + ***********************************/ + @Override + public boolean addQuartzJob(QuartzJob quartzJob) { + // 生成序号 + long orderNo = this.count() + 1L; + quartzJob.setOrderno((int) orderNo); + return this.save(quartzJob); + } + + /********************************** + * 用途说明: 删除定时任务 + * 参数说明 id id + * 返回值说明: boolean 是否成功 + ***********************************/ + @Override + public boolean deleteQuartzJob(String id) { + String[] split = id.split(","); + Set ids = Arrays.stream(split).collect(Collectors.toSet()); + for (String s : ids) { + QuartzJob quartzJob = this.getById(s); + quartzManage.deleteJob(quartzJob); + this.removeById(s); + } + + // 查询所有定时任务 + List list = + this.list(new LambdaQueryWrapper().orderByAsc(QuartzJob::getOrderno)); + // 更新序号 + for (int i = 0; i < list.size(); i++) { + QuartzJob quartzJob = list.get(i); + quartzJob.setOrderno(i + 1); + this.updateById(quartzJob); + } + return true; + } + + /********************************** + * 用途说明: 拖动修改定时任务顺序 + * 参数说明 fromID 当前ID toID 到达ID + * 返回值说明: com.yfd.platform.config.ResponseResult 返回拖动成功或者失败 + ***********************************/ + @Override + public boolean changeDictOrder(String fromID, String toID) { + QuartzJob fromQuartzJob = + quartzJobMapper.selectById(fromID); + QuartzJob toQuartzJob = quartzJobMapper.selectById(toID); + // 如果数据字典不存在拖动失败 + if (fromQuartzJob == null || toQuartzJob == null) { + return false; + } + Integer fromOrderNo = fromQuartzJob.getOrderno(); + Integer toOrderNo = toQuartzJob.getOrderno(); + // 如果数据字典的顺序号不存在拖动失败 + if (fromOrderNo == null || toOrderNo == null) { + return false; + } + // 将顺序号放入字典对象中 + fromQuartzJob.setOrderno(toOrderNo); + toQuartzJob.setOrderno(fromOrderNo); + // 更改顺序号 + boolean fromBool = this.updateById(fromQuartzJob); + boolean toBool = this.updateById(toQuartzJob); + return fromBool && toBool; + } + + /********************************** + * 用途说明: 执行定时任务 + * 参数说明 id id + * 返回值说明: void + ***********************************/ + @Override + public void execution(QuartzJob quartzJob) { + quartzManage.runJobNow(quartzJob); + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysConfigServiceImpl.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysConfigServiceImpl.java new file mode 100644 index 0000000..f26db95 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysConfigServiceImpl.java @@ -0,0 +1,34 @@ +package com.yfd.platform.system.system.service.impl; + + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.yfd.platform.system.domain.SysConfig; +import com.yfd.platform.system.mapper.SysConfigMapper; +import com.yfd.platform.system.service.ISysConfigService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import jakarta.annotation.Resource; +import javax.sound.sampled.UnsupportedAudioFileException; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + *

+ * 系统全局配置 服务实现类 + *

+ * + * @author zhengsl + * @since 2022-01-19 + */ +@Service +public class SysConfigServiceImpl extends ServiceImpl implements ISysConfigService { + @Resource + private UserServiceImpl currentUser; + + + + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysDictionaryItemsServiceImpl.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysDictionaryItemsServiceImpl.java new file mode 100644 index 0000000..7947a40 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysDictionaryItemsServiceImpl.java @@ -0,0 +1,123 @@ +package com.yfd.platform.system.system.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.yfd.platform.system.domain.SysDictionaryItems; +import com.yfd.platform.system.mapper.SysDictionaryItemsMapper; +import com.yfd.platform.system.service.ISysDictionaryItemsService; +import com.yfd.platform.utils.FileUtil; +import org.springframework.stereotype.Service; + +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + *

+ * 数据字典明细 服务实现类 + *

+ * + * @author TangWei + * @since 2023-03-08 + */ +@Service +public class SysDictionaryItemsServiceImpl extends ServiceImpl implements ISysDictionaryItemsService { + + @Resource + private SysDictionaryItemsMapper sysDictionaryItemsMapper; + + /********************************** + * 用途说明: 分页查询字典项信息 + * 参数说明 dictID 字典ID ItemName 字典项名称 pageNum 当前页 + * 返回值说明: com.yfd.platform.config.ResponseResult 返回分页查询结果 + ***********************************/ + @Override + public Page getDictItemPage(String dictId, + String itemName, + Page page) { + LambdaQueryWrapper queryWrapper = + new LambdaQueryWrapper<>(); + if (StrUtil.isNotBlank(itemName)) { + queryWrapper.like(SysDictionaryItems::getDictName, itemName); + } + queryWrapper.eq(SysDictionaryItems::getDictId, dictId).orderByAsc(SysDictionaryItems::getOrderNo); + return sysDictionaryItemsMapper.selectPage(page, queryWrapper); + } + + /********************************** + * 用途说明: 增加字典项 + * 参数说明 sysDictionaryItems 字典项信息 + * 返回值说明: com.yfd.platform.config.ResponseResult 返回增加成功或者失败 + ***********************************/ + @Override + public boolean addDictionaryItem(SysDictionaryItems sysDictionaryItems) { + LambdaQueryWrapper queryWrapper = + new LambdaQueryWrapper<>(); + queryWrapper.eq(SysDictionaryItems::getDictId,sysDictionaryItems.getDictId()); + long orderNo = this.count(queryWrapper) + 1L; + // 添加顺序号 + sysDictionaryItems.setOrderNo((int) orderNo); + return this.save(sysDictionaryItems); + } + + /********************************** + * 用途说明: 拖动修改字典项顺序 + * 参数说明 fromID 当前ID toID 到达ID + * 返回值说明: com.yfd.platform.config.ResponseResult 返回拖动成功或者失败 + ***********************************/ + @Override + public boolean changeItemOrder(String fromID, String toID) { + SysDictionaryItems fromSysDictionaryItems = + sysDictionaryItemsMapper.selectById(fromID); + SysDictionaryItems toSysDictionaryItems = + sysDictionaryItemsMapper.selectById(toID); + // 如果数据字典项不存在拖动失败 + if (fromSysDictionaryItems == null || toSysDictionaryItems == null) { + return false; + } + Integer fromOrderNo = fromSysDictionaryItems.getOrderNo(); + Integer toOrderNo = toSysDictionaryItems.getOrderNo(); + // 如果数据字典的顺序号不存在拖动失败 + if (fromOrderNo == null || toOrderNo == null) { + return false; + } + // 将顺序号放入字典对象中 + fromSysDictionaryItems.setOrderNo(toOrderNo); + toSysDictionaryItems.setOrderNo(fromOrderNo); + // 更改顺序号 + boolean fromBool = this.updateById(fromSysDictionaryItems); + boolean toBool = this.updateById(toSysDictionaryItems); + return fromBool && toBool; + + } + + /********************************** + * 用途说明: 导出数据字典项数据 + * 参数说明 sysDictionaryItemsList 所需导出的字典项集合 + * 返回值说明: com.yfd.platform.config.ResponseResult 返回导出成功或失败 + ***********************************/ + @Override + public void exportExcel(List sysDictionaryItems, + HttpServletResponse response) { + try { + List> list = new LinkedList<>(); + for (SysDictionaryItems sysDictionaryItem : sysDictionaryItems) { + Map map = new LinkedHashMap<>(); + map.put("项编号", sysDictionaryItem.getItemCode()); + map.put("项名称", sysDictionaryItem.getDictName()); + map.put("父编码", sysDictionaryItem.getParentCode()); + map.put("备注", sysDictionaryItem.getCustom1()); + list.add(map); + } + FileUtil.downloadExcel(list, response); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysDictionaryServiceImpl.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysDictionaryServiceImpl.java new file mode 100644 index 0000000..f6b3903 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysDictionaryServiceImpl.java @@ -0,0 +1,114 @@ +package com.yfd.platform.system.system.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.yfd.platform.system.domain.SysDictionary; +import com.yfd.platform.system.domain.SysDictionaryItems; +import com.yfd.platform.system.mapper.SysDictionaryItemsMapper; +import com.yfd.platform.system.mapper.SysDictionaryMapper; +import com.yfd.platform.system.service.ISysDictionaryService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.annotation.Resource; +import java.util.List; + +/** + *

+ * 数据字典表 服务实现类 + *

+ * + * @author TangWei + * @since 2023-03-08 + */ +@Service +public class SysDictionaryServiceImpl extends ServiceImpl implements ISysDictionaryService { + + @Resource + private SysDictionaryMapper sysDictionaryMapper; + + @Resource + private SysDictionaryItemsMapper sysDictionaryItemsMapper; + + /********************************** + * 用途说明: 获取数据字典列表 + * 参数说明 dictType 字典类型 + * 返回值说明: 返回字典列表集合 + ***********************************/ + @Override + public List getDictList(String dictType) { + LambdaQueryWrapper queryWrapper = + new LambdaQueryWrapper<>(); + queryWrapper.eq(SysDictionary::getDictType, dictType).orderByAsc(SysDictionary::getOrderNo); + return sysDictionaryMapper.selectList(queryWrapper); + } + + /********************************** + * 用途说明: 新增字典 + * 参数说明 sysDictionary 字典对象 + * 返回值说明: 返回增加成功或者失败 + ***********************************/ + @Override + public boolean addDict(SysDictionary sysDictionary) { + //int orderNo = this.count() + 1; + Integer maxNo = + sysDictionaryMapper.selectMaxNo(sysDictionary.getDictType()); + if (maxNo == null) { + maxNo = 0; + } + // 添加顺序号 + sysDictionary.setOrderNo(maxNo + 1); + return this.save(sysDictionary); + } + + /********************************** + * 用途说明: 根据ID删除字典 + * 参数说明 id 字典ID + * 返回值说明: com.yfd.platform.config.ResponseResult 返回删除结果成功或者失败 + ***********************************/ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean deleteDictById(String id) { + // 根据字典编码查询字典项中是否关联 + boolean isok=true; + QueryWrapper Wrapper = new QueryWrapper<>(); + Wrapper.eq("dictid", id); + if(sysDictionaryItemsMapper.delete(Wrapper)>0) { + isok=true; + } + return isok&&this.removeById(id); + } + + /********************************** + * 用途说明: 拖动修改字典顺序 + * 参数说明 fromID 当前ID toID 到达ID + * 返回值说明: com.yfd.platform.config.ResponseResult 返回拖动成功或者失败 + ***********************************/ + @Override + public boolean changeDictOrder(String fromID, String toID) { + SysDictionary fromSysDictionary = + sysDictionaryMapper.selectById(fromID); + SysDictionary toSysDictionary = sysDictionaryMapper.selectById(toID); + // 如果数据字典不存在拖动失败 + if (fromSysDictionary == null || toSysDictionary == null) { + return false; + } + Integer fromOrderNo = fromSysDictionary.getOrderNo(); + Integer toOrderNo = toSysDictionary.getOrderNo(); + // 如果数据字典的顺序号不存在拖动失败 + if (fromOrderNo == null || toOrderNo == null) { + return false; + } + // 将顺序号放入字典对象中 + fromSysDictionary.setOrderNo(toOrderNo); + toSysDictionary.setOrderNo(fromOrderNo); + // 更改顺序号 + boolean fromBool = this.updateById(fromSysDictionary); + boolean toBool = this.updateById(toSysDictionary); + return fromBool && toBool; + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysLogServiceImpl.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysLogServiceImpl.java new file mode 100644 index 0000000..e3546c0 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysLogServiceImpl.java @@ -0,0 +1,219 @@ +package com.yfd.platform.system.system.service.impl; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.yfd.platform.annotation.Log; +import com.yfd.platform.system.domain.SysLog; +import com.yfd.platform.system.mapper.SysLogMapper; +import com.yfd.platform.system.mapper.SysUserMapper; +import com.yfd.platform.system.service.ISysLogService; +import com.yfd.platform.utils.FileUtil; +import com.yfd.platform.utils.StringUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + +/** + *

+ * 系统操作日志 服务实现类 + *

+ * + * @author TangWei + * @since 2023-03-08 + */ +@Service +public class SysLogServiceImpl extends ServiceImpl implements ISysLogService { + + @Resource + private SysLogMapper sysLogMapper; + + /********************************** + * 用途说明: 分页查询日志信息 + * 参数说明 pageNum(页码数)、pageSize(页大小,如果是固定页大小可不传)、username(用户名)、(optType) + * 操作类型、startDate(开始日期)、endDate(结束日期) + * 返回值说明: com.yfd.platform.config.ResponseResult 返回分页查询结果 + ***********************************/ + @Override + public Page getLogList(String username, String optType, + String startDate, + String endDate, Page page) { + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + // 没有传username就不按此条件查询 + if (StrUtil.isNotBlank(username)) { + queryWrapper.like(SysLog::getUsername, username); + } + // 没有传optType就不按此条件查询 + if (StrUtil.isNotBlank(optType)) { + queryWrapper.eq(SysLog::getOpttype, optType); + } + DateTime parseStartDate = DateUtil.parse(startDate); + DateTime parseEndDate = DateUtil.parse(endDate); + DateTime dateTime = DateUtil.offsetDay(parseEndDate, 1); + if (parseStartDate != null && parseEndDate != null) { + queryWrapper.ge(SysLog::getLogtime, parseStartDate).lt(SysLog::getLogtime, dateTime); + } + + queryWrapper.orderByDesc(SysLog::getLogtime); + return sysLogMapper.selectPage(page, queryWrapper); + } + + /********************************** + * 用途说明: 导出日志数据 + * 参数说明 sysLogs 所需导出的字典项集合 + * 返回值说明: com.yfd.platform.config.ResponseResult 返回导出成功或者失败 + ***********************************/ + @Override + public void exportExcel(List sysLogs, + HttpServletResponse response) { + try { + List> list = new LinkedList<>(); + for (SysLog sysLog : sysLogs) { + Map map = new LinkedHashMap<>(); + map.put("操作账号", sysLog.getUsercode()); + map.put("用户姓名", sysLog.getUsername()); + map.put("IP地址", sysLog.getRequestip()); + map.put("浏览器", sysLog.getBrowser()); + map.put("日志类型", sysLog.getOpttype()); + map.put("模块名称", sysLog.getModule()); + map.put("日志描述", sysLog.getDescription()); + Timestamp logTime = sysLog.getLogtime(); + String dateTime = ""; + if (logTime != null) { + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd " + + "HH:mm:ss"); + dateTime = df.format(logTime); + } + + /*String dateTime = ""; + if (logTime != null) { + dateTime = logTime.format(DateTimeFormatter.ofPattern( + "yyyy-MM-dd HH:mm:ss")); + }*/ + + map.put("操作日期", dateTime); + list.add(map); + } + FileUtil.downloadExcel(list, response); + } catch (Exception e) { + e.printStackTrace(); + } + + } + + /********************************** + * 用途说明: 新增日志 + * 参数说明 nickname 用户名 + * 参数说明 username 用户账号 + * 参数说明 browser 浏览器 + * 参数说明 ip 本机Ip地址 + * 参数说明 joinPoint 连接点 + * 参数说明 log 日志信息 + * 返回值说明: void + ***********************************/ + @Override + public void save(String nickname, String username, String browser, + String ip, + ProceedingJoinPoint joinPoint, SysLog log) { + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + Log aopLog = method.getAnnotation(Log.class); + // 方法路径 + String methodName = + joinPoint.getTarget().getClass().getName() + "." + signature.getName() + "()"; + // 描述 + if (log != null) { + log.setDescription(aopLog.value()); + log.setModule(aopLog.module()); + } + assert log != null; + log.setUsercode(username); + log.setRequestip(ip); + log.setMethod(methodName); + log.setUsername(nickname); + log.setParams(getParameter(method, joinPoint.getArgs())); + log.setBrowser(browser); + String operationtype = getOperationtype(signature.getName()); + log.setOpttype(operationtype); + log.setLogtime(new Timestamp(System.currentTimeMillis())); + sysLogMapper.insert(log); + } + + /** + * 根据方法和传入的参数获取请求参数 + */ + private String getParameter(Method method, Object[] args) { + List argList = new ArrayList<>(); + Parameter[] parameters = method.getParameters(); + for (int i = 0; i < parameters.length; i++) { + //将RequestBody注解修饰的参数作为请求参数 + AnnotatedType type = parameters[i].getAnnotatedType(); + + RequestBody requestBody = + parameters[i].getAnnotation(RequestBody.class); + if (requestBody != null) { + argList.add(args[i]); + } + + //将RequestParam注解修饰的参数作为请求参数 + RequestParam requestParam = + parameters[i].getAnnotation(RequestParam.class); + if (requestParam != null) { + Map map = new HashMap<>(); + String key = parameters[i].getName(); + if (!StringUtils.isEmpty(requestParam.value())) { + key = requestParam.value(); + } + map.put(key, args[i]); + argList.add(map); + } + } + if (argList.size() == 0) { + return ""; + } + return argList.size() == 1 ? JSONUtil.toJsonStr(argList.get(0)) : + JSONUtil.toJsonStr(argList); + } + + public static String getOperationtype(String value) { + String type = ""; + if (value.contains("get") || value.contains("select")) { + type = "查询(select)"; + } else if (value.contains("add") || value.contains("insert")) { + type = "添加(insert)"; + } else if (value.contains("update") || value.contains("upd") || value.contains("change") || value.contains("set")) { + type = "修改(update)"; + } else if (value.contains("delete") || value.contains("del")) { + type = "删除(delete)"; + } else if (value.contains("dowload")) { + type = "下载(dowload)"; + } else if (value.contains("import")) { + type = "导入(import)"; + } else if (value.contains("word")) { + type = "word转pdf(wordToPdf)"; + } else { + type = "其他(other)"; + } + return type; + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysMenuServiceImpl.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysMenuServiceImpl.java new file mode 100644 index 0000000..aa8f1fe --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysMenuServiceImpl.java @@ -0,0 +1,695 @@ +package com.yfd.platform.system.system.service.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.yfd.platform.system.domain.SysMenu; +import com.yfd.platform.system.domain.SysRole; +import com.yfd.platform.system.mapper.SysMenuMapper; +import com.yfd.platform.system.mapper.SysRoleMapper; +import com.yfd.platform.system.service.ISysMenuService; +import com.yfd.platform.utils.FileUtil; +import com.yfd.platform.config.FileSpaceProperties; +import com.yfd.platform.utils.ObjectConverterUtil; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.ResourceUtils; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.annotation.Resource; +import java.io.File; +import java.io.FileNotFoundException; +import java.sql.Timestamp; +import java.text.DecimalFormat; +import java.util.*; +import java.util.stream.Collectors; + +/** + *

+ * 菜单及按钮 服务实现类 + *

+ * + * @author zhengsl + * @since 2021-12-15 + */ +@Service +@Transactional +public class SysMenuServiceImpl extends ServiceImpl implements ISysMenuService { + + @Resource + private SysMenuMapper sysMenuMapper; + + @Resource + private UserServiceImpl currentUser; + + @Resource + private SysRoleMapper sysRoleMapper; + + // 菜单图片路径配置 + @Resource + private FileSpaceProperties fileSpaceProperties; + + /*********************************** + * 用途说明:查询菜单及按钮树状图 + * 参数说明 + * systemcode 系统 + *isdisplay 是否显示 + * 返回值说明: 菜单结构树集合 + ***********************************/ + @Override + public List> getMenuButtonTree(String systemcode, + String name, + String isdisplay) { + List> mapList=null; + List> listMap =new ArrayList<>(); + if(StrUtil.isEmpty(name)){//不带名称查询,返回树结构 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("parentid", "0").eq("systemcode", systemcode).orderByAsc("orderno"); + mapList = this.listMaps(queryWrapper); + listMap.addAll(ObjectConverterUtil.convertMapFieldsToEntityFormat(SysMenu.class, mapList)); + for (int i = 0; i < listMap.size(); i++) { + //查询下一子集 + List> childList = child(listMap.get(i).get( + "id").toString(), systemcode, name, null, null); + listMap.get(i).put("children", childList); //添加新列 子集 + } + }else{ //根据菜单名称查询,直接返回类别 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.like("name", name).eq("systemcode", systemcode).orderByAsc("name"); + mapList = this.listMaps(queryWrapper); + listMap.addAll(ObjectConverterUtil.convertMapFieldsToEntityFormat(SysMenu.class, mapList)); + } + return listMap; + } + + /*********************************** + * 用途说明:获取菜单结构树(不含按钮) + * 参数说明 + * systemcode 系统 + * isdisplay 是否显示 + * 返回值说明: 菜单结构树集合 + ***********************************/ + @Override + public List> getMenuTree(String systemcode, + String name, + String isdisplay) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (StrUtil.isNotEmpty(isdisplay)) { + queryWrapper.eq("isdisplay", isdisplay); + } else { + queryWrapper.eq("isdisplay", 1); + } + + //根据系统 ,类型不为2 显示,序号 正序排序 + queryWrapper.eq("parentid", "0").eq("systemcode", systemcode).ne( + "type", "2").orderByAsc("orderno"); + List> listMap = this.listMaps(queryWrapper); + for (int i = 0; i < listMap.size(); i++) { + List> childList = child(listMap.get(i).get( + "id").toString(), systemcode, name, isdisplay, "2");//查询下一子集 + listMap.get(i).put("children", childList); //添加新列 子集 + } + return listMap; + } + + /*********************************** + * 用途说明:查询菜单及按钮树状图 + * 参数说明 + * parentid 上级id + *systemcode 系统 + * isdisplay 是否显示 + * type 按钮 + * 返回值说明: 菜单结构树集合 + ***********************************/ + private List> child(String parentid, + String systemcode, String name, + String isdisplay, String type) { + List> mapList; + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("parentid", parentid).eq("systemcode", systemcode); + //根据上级id 系统 查询 + if (StrUtil.isNotEmpty(type)) { + queryWrapper.ne("type", type); + } + + if (StrUtil.isNotEmpty(name)) { //根据菜单名称查询 + queryWrapper.like("name", name); + } + mapList = this.listMaps(queryWrapper.orderByAsc("orderno")); + List> listMap = ObjectConverterUtil.convertMapFieldsToEntityFormat(SysMenu.class, mapList); + if (!listMap.isEmpty()) { //判断是否存在子集 + for (int i = 0; i < listMap.size(); i++) { //遍历表数据 + List> childList = + child(listMap.get(i).get("id").toString(), systemcode + , name, isdisplay, type); //循环获取下一子集 + listMap.get(i).put("children", childList); //添加新列 子集 + } + } + + return listMap; + } + + /*********************************** + * 用途说明:新增菜单及按钮 + * 参数说明 + * sysMenu 菜单或按钮表对象 + * 返回值说明: 是否添加成功提示 + ***********************************/ + @Override + public boolean addMenu(SysMenu sysMenu) { + String parentId = sysMenu.getParentid(); + QueryWrapper queryWrapper = new QueryWrapper<>(); + //根据上级id 查询到总数 并累加 + long orderno = this.count(queryWrapper.eq("parentid", + parentId)) + 1L; + // 生成排序号 + sysMenu.setOrderno((int) orderno); + // 生成编号 + QueryWrapper queryMaxCode = new QueryWrapper<>(); + queryMaxCode.eq("parentid", parentId); + + // 使用 Oracle 兼容的 NVL 函数处理空值 + List maxList = this.listObjs( + queryMaxCode.select("NVL(MAX(code), '0') as code") + .eq("systemcode", sysMenu.getSystemcode()) + ); + SysMenu parentMenu = sysMenuMapper.selectById(parentId); + // 最大编号转换成int类型 + String maxCode = !maxList.isEmpty() ? maxList.getFirst().toString() : "0"; + int max = ObjectUtil.isEmpty(maxList) ? 0 : + Integer.parseInt(maxCode); + DecimalFormat df; + if ("0".equals(sysMenu.getParentid())) { + df = new DecimalFormat("00"); + } else if (parentMenu.getCode().length() == 2) { + df = new DecimalFormat("0000"); + } else { + df = new DecimalFormat("000000"); + } + //DecimalFormat df = new DecimalFormat("00"); + //int parentCode = Integer.parseInt(parentMenu.getCode()); + String parentCode = ""; + if (parentMenu != null) { + parentCode = parentMenu.getCode(); + } + // 生成的新编号 年月日+4位编号 + String code; + if (max > 0) { + code = df.format(max + 1); + } else { + max = max + 1; + if (StrUtil.isBlank(parentCode)) { + parentCode = "0" + max; + } else { + int i = Integer.parseInt(parentCode); + parentCode = i + "0" + max; + } + + int format = Integer.parseInt(parentCode); + code = df.format(format); + } + + // 判断是否显示字段 是否填写 为空 + if (StrUtil.isEmpty(sysMenu.getIsdisplay())) { + // 默认设置成 1 显示 + sysMenu.setIsdisplay("1"); + } + // 判断是否填写父级id 为空 默认设置成 0 + if (StrUtil.isEmpty(sysMenu.getParentid())) { + sysMenu.setParentid("0"); + } + // 添加编号 + sysMenu.setCode(code); + // 添加排序号 + sysMenu.setOrderno((int) orderno); + // 添加最近修改人 + sysMenu.setLastmodifier(currentUser.getUsername()); + // 添加最近修改时间 + sysMenu.setLastmodifydate(new Timestamp(System.currentTimeMillis())); + return this.save(sysMenu); + } + + /*********************************** + * 用途说明:上传单个图标 + * 参数说明 + * id 上传图标id + * icon 图标 + * 返回值说明: 是否上传成功 + ***********************************/ + @Override + public boolean uploadIcon(String id, MultipartFile icon) { + //根据id查询 + SysMenu sysMenu = this.getById(id); + //图片路径 + String iconPath = fileSpaceProperties.getSystem() + "menu" + File.separator; + String iconname = + IdUtil.fastSimpleUUID() + "." + FileUtil.getExtensionName(icon.getOriginalFilename()); + //上传图标并获取图标名称 (图片改为png格式) + String filename = + FileUtil.upload(icon, iconPath, iconname).getName(); + //更新图标名称 + sysMenu.setIcon(filename); + //添加最近修改人 + sysMenu.setLastmodifier(currentUser.getUsername()); + //添加最近修改时间 + sysMenu.setLastmodifydate(new Timestamp(System.currentTimeMillis())); + //更新数据 + boolean isOk = this.updateById(sysMenu); + return isOk; + } + + /*********************************** + * 用途说明:根据id删除单个图标 + * 参数说明 + * id 删除图标id + * icon 图标名称 + * 返回值说明: 是否删除成功 + ***********************************/ + @Override + public boolean deleteIcon(String id) { + //根据id查询 + SysMenu sysMenu = this.getById(id); + //图片路径 + String iconname = + System.getProperty("user.dir") + "\\src\\main" + + "\\resources\\static\\icon" + File.separator + sysMenu.getIcon(); + //更新图标名称 + sysMenu.setIcon(""); + //添加最近修改人 + sysMenu.setLastmodifier(currentUser.getUsername()); + //添加最近修改时间 + sysMenu.setLastmodifydate(new Timestamp(System.currentTimeMillis())); + //更新数据 + boolean isOk = this.updateById(sysMenu); + //更新成功 删除图片 + if (isOk == true) { + FileUtil.del(iconname); + } + return isOk; + } + + /*********************************** + * 用途说明:菜单及按钮序号排序 + * 参数说明 + * parentid 上级id + * id + * orderno 更改后序号 + * 返回值说明: 是否更新成功 + ***********************************/ + @Override + public boolean moveOrderno(String parentid, String id, int orderno) { + boolean ok = true; + SysMenu sysMenu = this.getById(id); //根据id查询原顺序号 + if (sysMenu.getOrderno() > orderno) { + ok = sysMenuMapper.upMoveOrderno(parentid, sysMenu.getOrderno(), + orderno); //根据 父级id 小于原序号 大于等于更改序号 + } else { + ok = sysMenuMapper.downMoveOrderno(parentid, sysMenu.getOrderno() + , orderno); //根据 父级id 大于原序号 小于等于更改序号 + } + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + ok = ok && this.update(updateWrapper.eq("id", id).set("orderno", + orderno)); //根据 id修改序号 + return ok; + } + + /*********************************** + * 用途说明:根据id删除菜单或按钮 + * 参数说明 + * id 删除列的id + * 返回值说明: 是否删除成功 + ***********************************/ + @Override + public boolean deleteById(String id) { + //根据id查询 + SysMenu sysMenu = this.getById(id); + //图片路径 + String iconname = + fileSpaceProperties.getSystem() + "menu" + File.separator + sysMenu.getIcon(); + //删除图标 + new File(iconname).delete(); + //根据id删除 + boolean isOk = this.removeById(id); + //删除成功同步更新表数据 + if (isOk) { + //1 创建list集合,用于封装所有删除目录或菜单id值 + List idList = new ArrayList<>(); + this.selectPermissionChildById(id, idList); + if (idList.size() > 0) { + sysMenuMapper.deleteBatchIds(idList); + } + QueryWrapper queryWrapper = new QueryWrapper<>(); + //根据上级id 查询 根据 orderno 正序排序 + queryWrapper.eq("parentid", sysMenu.getParentid()).orderByAsc( + "orderno"); + List list = this.list(queryWrapper); + for (int i = 0; i < list.size(); i++) { + SysMenu menu = list.get(i); + //更新序列号 + menu.setOrderno(i + 1); + } + //更新表数据 + this.updateBatchById(list); + } + return isOk; + } + + //2 根据当前菜单id,查询菜单里面子菜单id,封装到list集合 + private void selectPermissionChildById(String id, List idList) { + //查询菜单里面子菜单id + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("parentid", id); + wrapper.select("id"); + List childIdList = baseMapper.selectList(wrapper); + //把childIdList里面菜单id值获取出来,封装idList里面,做递归查询 + childIdList.stream().forEach(item -> { + //封装idList里面 + idList.add(item.getId()); + //递归查询 + this.selectPermissionChildById(item.getId(), idList); + }); + } + + /********************************** + * 用途说明: 调换菜单或按钮的位置 + * 参数说明 upperId 选中的菜单Id + * 参数说明 belowId 切换的菜单Id + * 返回值说明: boolean + ***********************************/ + @Override + @Transactional + public boolean changeOderNoById(String fromId, String toId) { + SysMenu fromSysMenu = this.getById(fromId); + SysMenu toSysMenu = this.getById(toId); + // 如果菜单或按钮不存在拖动失败 + if (fromSysMenu == null || toSysMenu == null) { + return false; + } + Integer fromOrderNo = fromSysMenu.getOrderno(); + Integer toOrderNo = toSysMenu.getOrderno(); + // 如果菜单或按钮的顺序号不存在拖动失败 + if (fromOrderNo == null || toOrderNo == null) { + return false; + } + fromSysMenu.setOrderno(toOrderNo); + toSysMenu.setOrderno(fromOrderNo); + boolean fromBool = this.updateById(fromSysMenu); + boolean toBool = this.updateById(toSysMenu); + return fromBool && toBool; + } + + /********************************** + * 用途说明: 根据用户id获取菜单树 + * 参数说明 id 用户id + * 返回值说明: 返回菜单树 + ***********************************/ + @Override + public List> getMenuTree(String id) { + // 根据id获取菜单 + //List sysMenus = sysMenuMapper.selectMenuByUserId(id); + List sysMenuList; + if (StrUtil.isBlank(id)) { + LambdaQueryWrapper queryWrapper = + new LambdaQueryWrapper<>(); + sysMenuList = this.list(queryWrapper.eq(SysMenu::getIsdisplay, "1").ne(SysMenu::getType, "2").eq(SysMenu::getSystemcode, "1").orderByAsc(SysMenu::getOrderno)); + } else { + sysMenuList = sysMenuMapper.selectMenuByUserId(id); + } + // 将 SysMenu 转换为 Map 并构建树 + List> list = sysMenuList.stream() + .map(menu -> { + Map map = new HashMap<>(); + map.put("id", menu.getId()); + map.put("parentid", menu.getParentid()); + map.put("name", menu.getName()); + map.put("opturl", menu.getOpturl()); + map.put("icon", menu.getIcon()); + map.put("type", menu.getType()); + map.put("systemcode", menu.getSystemcode()); + map.put("isdisplay", menu.getIsdisplay()); + map.put("orderno", menu.getOrderno()); + return map; + }) + .collect(Collectors.toList()); + + // 将菜单转换成树 + List> sysMenus = buildTreeLeft(list); + return sysMenus; + } + + /*********************************** + * 用途说明:权限分配 + * 参数说明 + * systemcode 系统 + * name 名称 + * isdisplay 是否显示 + * 返回值说明: 菜单结构树集合 + ***********************************/ + @Override + public List> permissionAssignment(String code,String roleId) { + +// String code = sysMenuMapper.getSystemCodeById(roleId); +// if (code == null) { +// code = "1"; +// } + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysMenu::getSystemcode, code).select(SysMenu::getId, + SysMenu::getParentid, SysMenu::getName).orderByAsc + (SysMenu::getOrderno); + List> mapList = + sysMenuMapper.selectMaps(queryWrapper); + List> listAll = ObjectConverterUtil.convertMapFieldsToEntityFormat(SysMenu.class, mapList); + List listRole = + sysMenuMapper.selectMenuByRoleId(roleId); + for (Map map : listAll) { + String id = (String) map.get("id"); + if (listRole.contains(id)) { + map.put("checkinfo", true); + } else { + map.put("checkinfo", false); + } + + } + List> listTree = buildTrees(listAll); + return listTree; + } + + // 另一种方法 + /*public List> permissionAssignment(String roleId) { + + String code = sysMenuMapper.getSystemCodeById(roleId); + if (code == null) { + code = "1"; + } + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysMenu::getSystemcode, code).select(SysMenu::getId, + SysMenu::getParentid, SysMenu::getName).orderByAsc + (SysMenu::getOrderno); + List> listAll = + sysMenuMapper.selectMaps(queryWrapper); + *//*List listRole = + sysMenuMapper.selectMenuByRoleId(roleId);*//* + SysRole sysRole = sysRoleMapper.selectById(roleId); + String optscope = sysRole.getOptscope(); + // 将当前角色所对应权限id拆分 + String[] split = optscope.split(","); + List listRole = Arrays.asList(split); + for (Map map : listAll) { + String id = (String) map.get("id"); + if (listRole.contains(id)) { + map.put("checkinfo", true); + } else { + map.put("checkinfo", false); + } + + } + List> listTree = buildTrees(listAll); + return listTree; + }*/ + + /*********************************** + * 用途说明:上传单个图标 + * 参数说明 + * icon 图标 + * 返回值说明: 是否上传成功 + ***********************************/ + @Override + public String uploadIcon(MultipartFile icon) throws FileNotFoundException { + + String path = System.getProperty("user.dir") + "\\src\\main" + + "\\resources\\"; + //图片路径 + String iconPath = path + "static\\icon" + File.separator; + String iconname = + IdUtil.fastSimpleUUID() + "." + FileUtil.getExtensionName(icon.getOriginalFilename()); + //上传图标并获取图标名称 (图片改为png格式) + String filename = + FileUtil.upload(icon, iconPath, iconname).getName(); + return filename; + } + + /** + * 菜单集合递归生成树状菜单(List) + * + * @param sysMenus 菜单对象 + * @return + */ + /* public List buildTree(List sysMenus) { + List resultMenuList = new ArrayList<>(); + for (SysMenu sysMenu : sysMenus) { + + for (SysMenu menu : sysMenus) { + if (menu.getParentid().equals(sysMenu.getId())) { + sysMenu.getChildren().add(menu); + } + } + if ("0".equals(sysMenu.getParentid())) { + resultMenuList.add(sysMenu); + } + } + return resultMenuList; + }*/ + + /** + * 菜单集合递归生成树状菜单(Map)(暂不使用该方法) + * + * @param sysMenus 菜单对象 + * @return + */ + public List> buildTree(List> sysMenus) { + List> resultMenuList = new ArrayList<>(); + for (Map sysMenu : sysMenus) { + + List> childrenList = new ArrayList<>(); + for (Map menu : sysMenus) { + if (menu.get("parentid").equals(sysMenu.get("id"))) { + childrenList.add(menu); + } + } + if ("0".equals(sysMenu.get("parentid"))) { + if (childrenList.size() > 0) { + sysMenu.put("children", childrenList); + } + resultMenuList.add(sysMenu); + } + } + return resultMenuList; + } + + /********************************** + * 用途说明: 左侧菜单树构建 + * 参数说明 sysMenus + * 返回值说明: java.util.List> + ***********************************/ + public List> buildTreeLeft(List> sysMenus) { + List> resultMenuList = new ArrayList<>(); + for (Map sysMenu : sysMenus) { + if ("0".equals(sysMenu.get("parentid"))) { + resultMenuList.add(sysMenu); + } + } + for (Map sysMenu : resultMenuList) { + List> menus = iterateMenusLeft(sysMenus, + (String) sysMenu.get("id")); + if (menus.size() > 0) { + sysMenu.put("children", menus); + } + } + return resultMenuList; + } + + /** + * 左侧多级菜单查询方法 + * + * @param menuVoList 不包含最高层次菜单的菜单集合 + * @param pid 父类id + * @return + */ + public List> iterateMenusLeft(List> menuVoList, String pid) { + List> result = new ArrayList<>(); + for (Map menu : menuVoList) { + //获取菜单的id + String menuid = (String) menu.get("id"); + //获取菜单的父id + String parentid = (String) menu.get("parentid"); + if (StrUtil.isNotBlank(parentid)) { + if (parentid.equals(pid)) { + //递归查询当前子菜单的子菜单 + List> iterateMenu = + iterateMenus(menuVoList, menuid); + if (iterateMenu.size() > 0) { + menu.put("children", iterateMenu); + } + result.add(menu); + } + } + } + return result; + } + + /********************************** + * 用途说明: 生成权菜单权限树 + * 参数说明 sysMenus + * 返回值说明: java.util.List> + ***********************************/ + public List> buildTrees(List> sysMenus) { + List> resultMenuList = new ArrayList<>(); + for (Map sysMenu : sysMenus) { + if ("0".equals(sysMenu.get("parentid"))) { + resultMenuList.add(sysMenu); + } + } + for (Map sysMenu : resultMenuList) { + List> menus = iterateMenus(sysMenus, + (String) sysMenu.get("id")); + for (Map menu : menus) { + if (!(boolean) menu.get("checkinfo")) { + sysMenu.put("checkinfo", false); + break; + } + } + sysMenu.put("children", menus); + } + return resultMenuList; + } + + /** + * 多级菜单查询方法 + * + * @param menuVoList 不包含最高层次菜单的菜单集合 + * @param pid 父类id + * @return + */ + public List> iterateMenus(List> menuVoList, String pid) { + List> result = new ArrayList<>(); + for (Map menu : menuVoList) { + //获取菜单的id + String menuid = (String) menu.get("id"); + //获取菜单的父id + String parentid = (String) menu.get("parentid"); + if (StrUtil.isNotBlank(parentid)) { + if (parentid.equals(pid)) { + //递归查询当前子菜单的子菜单 + List> iterateMenu = + iterateMenus(menuVoList, menuid); + for (Map map : iterateMenu) { + boolean checkinfo = (boolean) map.get("checkinfo"); + if (!checkinfo) { + menu.put("checkinfo", false); + } + } + menu.put("children", iterateMenu); + result.add(menu); + } + } + } + return result; + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysOrganizationServiceImpl.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysOrganizationServiceImpl.java new file mode 100644 index 0000000..684d0ba --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysOrganizationServiceImpl.java @@ -0,0 +1,363 @@ +package com.yfd.platform.system.system.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.yfd.platform.config.ResponseResult; +import com.yfd.platform.system.domain.SysOrganization; +import com.yfd.platform.system.domain.SysRole; +import com.yfd.platform.system.domain.SysUser; +import com.yfd.platform.system.mapper.SysOrganizationMapper; +import com.yfd.platform.system.mapper.SysRoleMapper; +import com.yfd.platform.system.service.ISysOrganizationService; +import com.yfd.platform.system.service.IUserService; +import com.yfd.platform.utils.ObjectConverterUtil; +import org.springframework.stereotype.Service; + +import jakarta.annotation.Resource; +import java.sql.Timestamp; +import java.text.DecimalFormat; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + *

+ * 系统组织框架 服务实现类 + *

+ * + * @author zhengsl + * @since 2021-12-15 + */ +@Service +public class SysOrganizationServiceImpl extends ServiceImpl implements ISysOrganizationService { + + @Resource + private UserServiceImpl currentUser; + + @Resource + private IUserService userService; + + @Resource + private SysRoleMapper sysRoleMapper; + + @Resource + private SysOrganizationMapper sysOrganizationMapper; + + /*********************************** + * 用途说明:获取组织结构树 + * 参数说明 + *parentid 上级id + * params 名称(根据名称查询二级) + * 返回值说明: 组织树集合 + ***********************************/ + @Override + public List> getOrgTree(String parentid, + String params) { + List orgList = new ArrayList<>(); + QueryWrapper queryWrapper = new QueryWrapper<>(); + //根据父级id查询 + queryWrapper.eq("parentid", parentid); + if (StrUtil.isNotEmpty(params)) { + queryWrapper.like("orgname", params); // 根据 部门名称 + } + SysUser userInfo = userService.getUserInfo(); + if (userInfo.getUsertype() != 0) { + List roleByUserId = + sysRoleMapper.getRoleByUserId(userInfo.getId()); + List ids = new ArrayList<>(); + // 循环当前角色 + for (SysRole sysRole : roleByUserId) { + // 获取角色的组织Id + String orgscope = sysRole.getOrgscope(); + if (StrUtil.isBlank(orgscope)) { + continue; + } + // 拆分组织Id + String[] split = orgscope.split(","); + List stringList = Arrays.asList(split); + Set set = new HashSet<>(); + if (stringList.size() > 0) { + List list = + sysOrganizationMapper.selectList(new LambdaQueryWrapper().in(SysOrganization::getId, stringList)); + list.forEach(l -> set.add(l.getParentid())); + } + ids.addAll(stringList); + ids.addAll(set); + } + queryWrapper.in("id", ids); + } + orgList = this.list(queryWrapper.orderByAsc("orgcode")); + // 将实体对象转换为 Map,确保字段名与实体类一致 + // 将实体对象转换为 Map,确保字段名与实体类一致 + List> listMap = orgList.stream().map(org -> { + Map map = new HashMap<>(); + map.put("id", org.getId()); + map.put("orgtype", org.getOrgtype()); + map.put("orgcode", org.getOrgcode()); + map.put("orgname", org.getOrgname()); + map.put("parentid", org.getParentid()); + map.put("manager", org.getManager()); + map.put("isvaild", org.getIsvaild()); + map.put("description", org.getDescription()); + map.put("lastmodifier", org.getLastmodifier()); + map.put("lastmodifydate", org.getLastmodifydate()); + map.put("custom1", org.getCustom1()); + map.put("custom2", org.getCustom2()); + map.put("custom3", org.getCustom3()); + return map; + }).collect(Collectors.toList()); + for (Map map : listMap) { + List> childList = child(map.get( + "id").toString());//查询下一子集 + map.put("childList", childList); //添加新列 子集 + } + return listMap; + } + + /*********************************** + * 用途说明:获取组织结构树 + * 参数说明 + *parentid 上级id + * params (根据参数查询 组织名称 负责人 描述) + * 返回值说明: 组织树集合 + ***********************************/ + private List> child(String parentid) { + List> listMap = new ArrayList<>(); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("parentid", parentid); //根据上级id 查询 + listMap = this.listMaps(queryWrapper.orderByAsc("orgcode")); + + if (!listMap.isEmpty()) { + List> mapList = ObjectConverterUtil.convertMapFieldsToEntityFormat(SysOrganization.class, listMap);//判断是否存在子集 + for (int i = 0; i < mapList.size(); i++) { //遍历表数据 + List> childList = + child(mapList.get(i).get("id").toString()); //循环获取下一子集 + mapList.get(i).put("childList", childList); //添加新列 子集 + } + return mapList; + } + return listMap; + } + + /*********************************** + * 用途说明:新增系统组织框架 + * 参数说明 + * sysOrganization 系统组织框架对象 + * 返回值说明: 是否新增成功 + ***********************************/ + @Override + public boolean addOrg(SysOrganization sysOrganization) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + SysOrganization parent = null; + int codeMax = 0; + //查询最大的编号 判断是否存在父级id 有值 根据父级id查询 否则 根据父级id为0 查询 + queryWrapper.select("max(orgcode)"); + if (StrUtil.isNotEmpty(sysOrganization.getParentid())) { + //根据父级id查询父级信息 + parent = this.getById(sysOrganization.getParentid()); + queryWrapper.eq("parentid", sysOrganization.getParentid()); + } else { + //默认 填写父级id为0 + sysOrganization.setParentid("0"); + queryWrapper.eq("parentid", "0"); + } + List max = this.listObjs(queryWrapper); + //判断查询是否存在 存在转换成 int 类型并给 codeMax 替换值 + if (!max.isEmpty() && max.getFirst() != null) { + String maxValue = max.getFirst().toString(); + if (maxValue.length() >= 2) { + codeMax = Integer.parseInt(maxValue.substring(maxValue.length() - 2)); + } + } + //2位数字编号 + DecimalFormat df = new DecimalFormat("00"); + //编号 + String code = df.format(codeMax + 1); + //查询到父级不为空 重新赋值 父级编号+新的序号 + if (parent != null) { + code = parent.getOrgcode() + df.format(codeMax + 1); + } + //判断是否是否填写 有效 否则默认为 1 + if (StrUtil.isEmpty(sysOrganization.getIsvaild())) { + sysOrganization.setIsvaild("1"); + } + //填写 编号 + sysOrganization.setOrgcode(code); + //填写 当前用户名称 + sysOrganization.setLastmodifier(currentUser.getUsername()); + //填写 当前日期 + sysOrganization.setLastmodifydate(new Timestamp(System.currentTimeMillis())); + return this.save(sysOrganization); + } + + /*********************************** + * 用途说明:根据企业ID查询组织详情 + * 参数说明 + * id 企业id + * 返回值说明: 系统组织框架对象 + ***********************************/ + @Override + public List getOrganizationById(String id, + String orgName) { + + LambdaQueryWrapper queryWrapper = + new LambdaQueryWrapper<>(); + SysUser userInfo = userService.getUserInfo(); + if (userInfo.getUsertype() != 0) { + List roleByUserId = + sysRoleMapper.getRoleByUserId(userInfo.getId()); + List ids = new ArrayList<>(); + // 循环当前角色 + for (SysRole sysRole : roleByUserId) { + // 获取角色的组织Id + String orgscope = sysRole.getOrgscope(); + if (StrUtil.isBlank(orgscope)) { + continue; + } + // 拆分组织Id + String[] split = orgscope.split(","); + List stringList = Arrays.asList(split); + ids.addAll(stringList); + } + if (ObjectUtil.isNotEmpty(ids)) { + queryWrapper.in(SysOrganization::getId, ids); + } + + } + if (StrUtil.isNotBlank(orgName)) { + queryWrapper.like(SysOrganization::getOrgname, orgName); + } + queryWrapper.eq(SysOrganization::getParentid, id).orderByDesc(SysOrganization::getOrgcode); + return this.list(queryWrapper); + } + + /*********************************** + * 用途说明:获取组织范围树结构 + * 参数说明 + *parentid 上级id + * params 名称(根据名称查询二级) + * 返回值说明: 组织树集合 + ***********************************/ + @Override + public List> getOrgScopeTree(String roleId) { + LambdaQueryWrapper queryWrapper = + new LambdaQueryWrapper<>(); + queryWrapper.eq(SysOrganization::getIsvaild, '1'); + queryWrapper.orderByAsc(SysOrganization::getOrgcode); + List> listMaps = this.listMaps(queryWrapper); + // 获取当前角色 + SysRole sysRole = sysRoleMapper.selectById(roleId); + String orgscope = sysRole.getOrgscope(); + List ids = new ArrayList<>(); + if (StrUtil.isNotBlank(orgscope)) { + String[] split = orgscope.split(","); + ids = Arrays.asList(split); + } + + for (Map map : listMaps) { + String id = (String) map.get("id"); + if (ids.contains(id)) { + map.put("checkinfo", true); + } else { + map.put("checkinfo", false); + } + map.put("bool", true); + } + // 生成组织树 + List> listMap = buildTrees(listMaps); + return listMap; + } + + /********************************** + * 用途说明: 修改角色组织范围 + * 参数说明 roleId 角色id + * 参数说明 orgscope 组织id集合 + * 返回值说明: boolean 是否修改成功 + ***********************************/ + @Override + public boolean updateOrgScopeByRoleId(String roleId, String orgscope) { + SysRole sysRole = new SysRole(); + sysRole.setId(roleId); + sysRole.setOrgscope(orgscope); + int i = sysRoleMapper.updateById(sysRole); + if (i > 0) { + return true; + } else { + return false; + } + + } + + /********************************** + * 用途说明: 生成组织范围树 + * 参数说明 sysMenus + * 返回值说明: java.util.List> + ***********************************/ + public List> buildTrees(List> sysMenus) { + List> resultMenuList = new ArrayList<>(); + // 获取父节点 + for (Map sysMenu : sysMenus) { + if ("0".equals(sysMenu.get("parentid"))) { + resultMenuList.add(sysMenu); + } + } + // 寻找子节点 + for (Map sysMenu : resultMenuList) { + sysMenu.put("checkinfo", true); + List> children = new ArrayList<>(); + List array = new ArrayList<>(); + for (Map menu : sysMenus) { + String id = (String) sysMenu.get("id"); + String parentid = (String) menu.get("parentid"); + if (id.equals(parentid)) { + // 如果存在一个子节点没有被选中,父节点则不是全选状态 + if (!(boolean) menu.get("checkinfo")) { + sysMenu.put("checkinfo", false); + } else { + // 将处于选中状态的子节点加入到数组中 + array.add((String) menu.get("orgname")); + } + children.add(menu); + } + } + // 所有子节点加入父节点 + sysMenu.put("children", children); + sysMenu.put("array", array); + } + return resultMenuList; + } + + /** + * 组织集合递归生成树状菜单(Map) + * + * @param sysOrgList 组织集合 + * @return + */ + public List> buildTree(List> sysOrgList) { + List> resultOrgList = new ArrayList<>(); + for (Map sysOrg : sysOrgList) { + List> childrenList = new ArrayList<>(); + List array = new ArrayList<>(); + for (Map org : sysOrgList) { + if (org.get("parentid").equals(sysOrg.get("id"))) { + if (!(boolean) org.get("checkinfo")) { + sysOrg.put("checkinfo", false); + } + array.add((String) org.get("orgname")); + childrenList.add(org); + } + } + if ("0".equals(sysOrg.get("parentid"))) { + if (childrenList.size() > 0) { + sysOrg.put("children", childrenList); + } + resultOrgList.add(sysOrg); + } + sysOrg.put("array", array); + } + return resultOrgList; + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysRoleServiceImpl.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysRoleServiceImpl.java new file mode 100644 index 0000000..522845d --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/SysRoleServiceImpl.java @@ -0,0 +1,174 @@ +package com.yfd.platform.system.system.service.impl; + +import cn.hutool.core.util.IdUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.yfd.platform.config.ResponseResult; +import com.yfd.platform.system.domain.SysRole; +import com.yfd.platform.system.mapper.SysRoleMapper; +import com.yfd.platform.system.service.ISysRoleService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.annotation.Resource; +import java.sql.Timestamp; +import java.text.DecimalFormat; +import java.util.List; +import java.util.Map; + +/** + *

+ * 系统角色 服务实现类 + *

+ * + * @author zhengsl + * @since 2021-12-15 + */ +@Service +@Transactional +public class SysRoleServiceImpl extends ServiceImpl implements ISysRoleService { + + @Resource + private SysRoleMapper roleMapper; + + @Resource + private UserServiceImpl currentuser; + + /*********************************** + * 用途说明:新增角色 + * 参数说明 + * sysRole 新增角色信息 + * 返回值说明: 是否新增成功 + ***********************************/ + @Override + public boolean addRole(SysRole sysRole) { + //生成用户编号 + int codeMax = 0; + DecimalFormat df = new DecimalFormat("000");//四位数字编号 + QueryWrapper queryWrapper = new QueryWrapper<>(); + List max = this.listObjs(queryWrapper.select("MAX(rolecode) " + + "rolecode"));// 查询最大的编号 + // 存在转换成 int 类型并给 codeMax 替换值 + if (!max.isEmpty() && max.getFirst() != null) { + try { + codeMax = Integer.parseInt(max.getFirst().toString()); + } catch (NumberFormatException e) { + // 如果转换失败,保持默认值 0 + e.printStackTrace(); + } + } + // 存在转换成int类型并给codeMax替换值 + String code = df.format(codeMax + 1); // 最大编号累加 + + sysRole.setRolecode(code); //添加角色编号 + if (StringUtils.isEmpty(sysRole.getIsvaild())) { + sysRole.setIsvaild("1"); //判断是否填写有效性 默认为 1 是 + } + sysRole.setLastmodifier(currentuser.getUsername()); //添加最近修改者 + sysRole.setLastmodifydate(new Timestamp(System.currentTimeMillis())); //添加最近修改时间 + return this.save(sysRole); + } + + /*********************************** + * 用途说明:删除角色用户(admin除外) + * 参数说明 + * id 系统角色用户对照表id + * 返回值说明: 是否新增成功 + ***********************************/ + @Override + public boolean deleteRoleUsers(String roleid, String urserids) { + boolean ok = true; + //得到单个用户id + String[] temp = urserids.split(","); + for (String userid : temp) { + //根据角色id、用户id删除 (登录账号admin除外) + ok = ok && roleMapper.deleteRoleUsers(roleid, userid); + + } + return ok; + } + + /*********************************** + * 用途说明:根据id删除角色 //待修改 + * 参数说明 + *id 角色id + * 返回值说明: 是否删除成功 + ***********************************/ + @Override + public void deleteById(String id) { + String[] ids = id.split(","); + for (String roleId : ids) { + //根据id删除 角色 + boolean isOk = this.removeById(roleId); + if (!isOk) { + continue; + } + roleMapper.deleteRoleMenus(roleId); + roleMapper.deleteRoleUser(roleId); + } + } + /* 原逻辑 + @Override + public boolean deleteById(String id) { + //根据角色id查询 所关联的用户 + List> isRoleUsersByroleid = + roleMapper.isRoleUsersByroleid(id); + //判断是否关联用户 + if (isRoleUsersByroleid.size() > 0) { + return false; + } + //根据id删除 角色 + boolean isOk = this.removeById(id); + if (isOk) { + return true; + } + return false; + }*/ + + /*********************************** + * 用途说明:查询已分配的用户 + * 参数说明 + *orgid 所属组织 + *username 用户名称 + *status 状态 + *level 角色级别 + * rolename 角色名称 + * isvaild 角色是否有效 + * 返回值说明: 系统用户角色数据集合 + ***********************************/ + @Override + public List listRoleUsers(String orgid, String username, + String status, String level, + String rolename, String isvaild) { + return roleMapper.listRoleUsers(orgid, username, status, level, + rolename, isvaild); + } + + /*********************************** + * 用途说明:角色分配权限 + * 参数说明 + * id 角色id + * menuIds 权限id字符串 + * 返回值说明: 是否分配成功 + ***********************************/ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean setMenuById(String id, String menuIds) { + // 删除角色所对应的权限 + roleMapper.deleteRoleMenus(id); + // 重新赋予权限 + String[] ids = menuIds.split(","); + for (String menuId : ids) { + String uuid = IdUtil.fastSimpleUUID(); + roleMapper.addRoleMenu(uuid, id, menuId); + } + return true; + } + + @Override + public List selectRoleList(String rolename) { + return roleMapper.selectRoleList(rolename); + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/UserDetailsServiceImpl.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/UserDetailsServiceImpl.java new file mode 100644 index 0000000..e02fd0c --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/UserDetailsServiceImpl.java @@ -0,0 +1,52 @@ +package com.yfd.platform.system.system.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.yfd.platform.datasource.DataSource; +import com.yfd.platform.system.domain.LoginUser; +import com.yfd.platform.system.domain.SysUser; +import com.yfd.platform.system.mapper.SysMenuMapper; +import com.yfd.platform.system.service.IUserService; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import jakarta.annotation.Resource; +import java.util.List; + +/** + *

+ * 用户服务实现类 继承UserDetailsService 实现接口 + *

+ * + * @author zhengsl + * @since 2021-10-27 + */ +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + + @Resource + private IUserService userService; + + @Resource + private SysMenuMapper sysMenuMapper; + + @Override + @DataSource(name = "master") + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + //根据用户名称查询用户信息 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("username", username); + SysUser user = userService.getOne(queryWrapper); + if (ObjectUtil.isEmpty(user)) { + throw new RuntimeException("用户账号不存在!"); + } + //Todo 根据用户查询权限信息 添加到LoginUser中 + List permissions = + sysMenuMapper.selectPermsByUserId(user.getId()); + + //封装成UserDetails对象返回 + return new LoginUser(user,permissions); + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/UserServiceImpl.java b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..3248743 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/system/service/impl/UserServiceImpl.java @@ -0,0 +1,579 @@ +package com.yfd.platform.system.system.service.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.yfd.platform.config.FileProperties; +import com.yfd.platform.config.ResponseResult; +import com.yfd.platform.system.domain.LoginUser; +import com.yfd.platform.system.domain.SysOrganization; +import com.yfd.platform.system.domain.SysRole; +import com.yfd.platform.system.domain.SysUser; +import com.yfd.platform.system.mapper.SysOrganizationMapper; +import com.yfd.platform.system.mapper.SysRoleMapper; +import com.yfd.platform.system.mapper.SysUserMapper; +import com.yfd.platform.system.service.IUserService; +import com.yfd.platform.utils.FileUtil; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import com.yfd.platform.config.FileSpaceProperties; +import jakarta.annotation.Resource; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.annotation.Resource; +import jakarta.validation.constraints.NotBlank; +import java.io.File; +import java.sql.Timestamp; +import java.util.*; +import java.util.stream.Collectors; + +/** + *

+ * 用户服务实现类 + *

+ * + * @author zhengsl + * @since 2021-10-27 + */ +@Service +@RequiredArgsConstructor +public class UserServiceImpl extends ServiceImpl implements IUserService { + + @Resource + private SysUserMapper sysUserMapper; + + @Resource + private SysRoleMapper sysRoleMapper; + + @Resource + private PasswordEncoder passwordEncoder; + /** + * 文件空间配置 + */ + @Resource + private FileSpaceProperties fileSpaceProperties; + + /********************************** + * 用途说明:获取当前用户账号及名称 + * 参数说明 + * 返回值说明: 系统管理员[admin] + ***********************************/ + @Override + public String getUsername() { + UsernamePasswordAuthenticationToken authentication = + (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + LoginUser loginuser = (LoginUser) authentication.getPrincipal(); + String acountname = + loginuser.getUser().getNickname(); + return acountname; + //return "admin"; + } + + @Override + public Map getNameInfo() { + UsernamePasswordAuthenticationToken authentication = + (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + LoginUser loginuser = (LoginUser) authentication.getPrincipal(); + String nickname = loginuser.getUser().getNickname(); + String username = loginuser.getUser().getUsername(); + Map map = new HashMap<>(); + map.put("nickname", nickname); + map.put("username", username); + return map; + } + + @Override + public SysUser getUserInfo() { + UsernamePasswordAuthenticationToken authentication = + (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + LoginUser loginuser = (LoginUser) authentication.getPrincipal(); + return loginuser.getUser(); + } + + @Override + public ResponseResult getLoginUserInfo() { + UsernamePasswordAuthenticationToken authentication = + (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + LoginUser loginuser = (LoginUser) authentication.getPrincipal(); + SysUser user = loginuser.getUser(); + //根据用户ID获取组织 + Map userInfo = + sysUserMapper.getOrganizationByid(user.getId()); + + // 使用 MyBatis 方式查询用户角色(Oracle 兼容) + List roles = sysRoleMapper.getRoleByUserId(user.getId()); + List collect = + roles.stream().map(SysRole::getRolename).collect(Collectors.toList()); + ResponseResult responseResult = new ResponseResult(); + responseResult.put("userInfo", userInfo); + responseResult.put("roles", collect); + responseResult.put("permissions", loginuser.getPermissions()); + return responseResult; + } + + /*********************************** + * 用途说明:新增用户 + * 参数说明 + *sysUser 新增用户对象 + * id 创建者id + * roleId 角色id + * 返回值说明: 提示字符串 + ************************************/ + @Override + public Map addUser(SysUser sysUser, String roleids) { + //返回信息 + Map result = new HashMap<>(); + sysUser.setId(IdUtil.fastSimpleUUID()); + //普通用户 + sysUser.setUsertype(1); + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + //设置缺省密码 + String cryptPassword = passwordEncoder.encode("123456"); + sysUser.setPassword(cryptPassword); + sysUser.setLastmodifydate(new Timestamp(System.currentTimeMillis())); + sysUser.setLastmodifier(getUsername()); + //账号有效 + sysUser.setStatus(1); + //判断注册的登录账号是否存在 + if (isExistAccount(sysUser.getUsername())) { + //新增用户 + boolean ok = this.save(sysUser); + //新增用户分配权限 + if (StrUtil.isNotEmpty(roleids)) { + String[] roles = roleids.split(","); + for (String roleid : roles) { + //系统生成id + String id = IdUtil.fastSimpleUUID(); + //新增sys_role_users表数据 + ok = ok && sysUserMapper.addUserRoles(id, roleid, + sysUser.getId()); + } + } + //判断新增是否成功 消息提示 + if (ok) { + result.put("status", "sucess"); + result.put("msg", "新增用户成功!"); + + } else { + result.put("status", "error"); + result.put("msg", "新增用户失败!"); + } + } else { + result.put("status", "error"); + result.put("msg", "用户账号已存在,不能重复添加!"); + } + return result; + } + + /*********************************** + * 用途说明:查询系统用户 + * 参数说明 + *page 分页集合参数 + *orgid 所属组织 + *username 用户名称 + * mobile 手机号 + * status 状态 + * 返回值说明: 用户分页集合 + ************************************/ + @Override + public List list(String total, String size, String orgid, + String username, String mobile, String status) { + List list = sysUserMapper.list(total, size, orgid, username, + mobile, status); + for (Map map : list) { + List mapList = + sysUserMapper.getLevel(map.get("id").toString()); + String roleid = ""; + String level = ""; + String rolename = ""; + for (Map map1 : mapList) { + roleid += map1.get("id") + ","; + level += map1.get("level") + ","; + rolename += map1.get("rolename") + ","; + } + if (roleid.endsWith(",")) { + roleid = roleid.substring(0, roleid.length() - 1); + } + if (level.endsWith(",")) { + level = level.substring(0, level.length() - 1); + } + if (rolename.endsWith(",")) { + rolename = rolename.substring(0, rolename.length() - 1); + } + + map.put("roleid", roleid); + map.put("level", level); + map.put("rolename", rolename); + } + return list; //返回分页集合 + } + + /*********************************** + * 用途说明:根据ID修改用户 + * 参数说明 + *sysUser 用户对象 + *roleids 角色id + * 返回值说明: 是否更新成功 + ************************************/ + // ... existing code ... + + @Override + @Transactional(rollbackFor = Exception.class) + public Map updateById(SysUser sysUser, String roleids) { + Map result = new HashMap<>(); + + try { + // 设置修改信息 + String currentUsername = getUsername(); + Timestamp currentTime = new Timestamp(System.currentTimeMillis()); + sysUser.setLastmodifier(currentUsername); + sysUser.setLastmodifydate(currentTime); + + // 更新用户信息 + boolean ok = this.updateById(sysUser); + if (!ok) { + result.put("status", "error"); + result.put("msg", "用户信息修改失败!"); + return result; + } + + // 处理角色分配 + String userId = sysUser.getId(); + if (StrUtil.isNotEmpty(roleids)) { + handleUserRoles(userId, roleids); + } else { + // 清空所有角色 + sysUserMapper.delRoleUsersByUserid(userId); + } + + result.put("status", "sucess"); + result.put("msg", "用户信息修改成功!"); + + } catch (Exception e) { + log.error("更新用户信息失败", e); + result.put("status", "error"); + result.put("msg", "操作失败:" + e.getMessage()); + throw e; // 抛出异常,触发事务回滚 + } + + return result; + } + + /** + * 处理用户角色分配(增量更新) + * @param userId 用户 ID + * @param roleIds 角色 ID 字符串(逗号分隔) + */ + private void handleUserRoles(String userId, String roleIds) { + // 获取用户当前角色 + List currentRoles = sysUserMapper.getRoleid(userId); + Set currentRoleSet = new HashSet<>(currentRoles != null ? currentRoles : Collections.emptyList()); + + // 解析新角色列表 + String[] newRoles = roleIds.split(","); + Set newRoleSet = new HashSet<>(Arrays.asList(newRoles)); + + // 需要新增的角色(新角色 - 当前角色) + for (String roleId : newRoles) { + if (!currentRoleSet.contains(roleId)) { + String id = IdUtil.fastSimpleUUID(); + sysUserMapper.addUserRoles(id, roleId, userId); + } + } + + // 需要删除的角色(当前角色 - 新角色) + sysUserMapper.delInRoleUsersByUserid(userId, newRoles); + } + +// ... existing code ... + + + @Override + public Map getOneById(String id) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + Map map = this.getMap(queryWrapper.eq("id", id)); + List mapList = sysUserMapper.getLevel(id); + String roleid = ""; + String level = ""; + String rolename = ""; + for (Map map1 : mapList) { + roleid += map1.get("id") + ","; + level += map1.get("level") + ","; + rolename += map1.get("rolename") + ","; + } + if (roleid.endsWith(",")) { + roleid = roleid.substring(0, roleid.length() - 1); + } + if (level.endsWith(",")) { + level = level.substring(0, level.length() - 1); + } + if (rolename.endsWith(",")) { + rolename = rolename.substring(0, rolename.length() - 1); + } + + map.put("roleid", roleid); + map.put("level", level); + map.put("rolename", rolename); + return map; + } + + /*********************************** + * 用途说明:用户分配角色 + * 参数说明 + *listId 用户id与角色id + * 返回值说明: 判断是否添加成功 + ************************************/ + @Override + public boolean setUserRoles(String roleid, String userids) { + boolean isOk = true; + //拆分userid 数组 + String[] temp = userids.split(","); + //遍历userid + for (String userid : temp) { + //根据角色id与用户id查询 + List list = sysUserMapper.getRoleUsersByid(roleid, userid); + //判断是否用户已分配此权限 + if (list.size() == 0) { + //系统生成id + String id = IdUtil.fastSimpleUUID(); + //新增sys_role_users表数据 + isOk = isOk && sysUserMapper.addUserRoles(id, roleid, userid); + } + } + return isOk; + } + + /*********************************** + * 用途说明:根据id删除用户 + * 参数说明 + *id 用户id + * 返回值说明: 判断是否删除成功 + ************************************/ + @Override + public boolean deleteById(String id) { + //根据id查询 + SysUser sysUser = this.getById(id); + //账号头像存储地址 + String imgName = + fileSpaceProperties.getSystem() + File.separator + "user" + File.separator + sysUser.getAvatar(); + if ("admin".equals(sysUser.getUsername())) { + return false; + } else { + boolean isOk = this.removeById(id); + //判断是否删除成功 + if (isOk) { + //根据用户id 删除该用户角色关联 + sysUserMapper.delRoleUsersByUserid(id); + //判断是否存在 账号头像 存在删除 + if (StrUtil.isNotEmpty(sysUser.getAvatar())) { + FileUtil.del(imgName); + } + return false; + } else { + return false; + } + } + } + + /*********************************** + * 用途说明:重置用户密码(管理员) + * 参数说明 + *id 重置密码的 用户id + * 返回值说明: 判断是重置成功 + ************************************/ + @Override + public boolean resetPassword(String id) throws Exception { + boolean isOk = false; + //根据当前用户id 查询角色表的级别 currentUser.getUser() 获取当前用户id + String level = sysUserMapper.getMaxLevel(id); + //判断是否获取级别 + if (StrUtil.isNotEmpty(level)) { + //判断当前用户级别 管理员及以上权限 + if (Integer.parseInt(level) <= 2) { + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + //根据id 修改密码,密码修改时间,最近修改者,最近修改日期 将密码修改为 123456 + String cryptPassword = passwordEncoder.encode("123456"); + updateWrapper.eq("id", id).set("password", cryptPassword).set( + "pwdresettime", + new Timestamp(System.currentTimeMillis())).set( + "lastmodifydate", + new Timestamp(System.currentTimeMillis())).set( + "lastmodifier", getUsername()); + //是否修改成功 + isOk = this.update(updateWrapper); + } + } + return isOk; + } + + /*********************************** + * 用途说明:设置账号状态(管理员) + * 参数说明 + *id 用户id + * status 设置状态 + * 返回值说明: 判断是否设置成功 + ************************************/ + @Override + public boolean setStatus(String id, String status) { + boolean isOk = false; + //根据当前用户id 查询角色表的级别 currentUser.getUser() 获取当前用户id + String level = sysUserMapper.getMaxLevel(id); + //判断当前用户级别 管理员及以上权限 + if (Integer.parseInt(level) <= 2) { + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + //根据id修改用户状态,最近修改人,最近修改时间 + updateWrapper.eq("id", id).set("status", status).set( + "lastmodifydate", + new Timestamp(System.currentTimeMillis())).set( + "lastmodifier", getUsername()); + //是否修改成功 + isOk = this.update(updateWrapper); + } + return isOk; + } + + /*********************************** + * 用途说明:上传用户头像 + * 参数说明 + * id 用户id + * img 账号头像 + * 返回值说明: 判断是否上传 + ***********************************/ + @Override + public boolean uploadAvatar(String id, MultipartFile img) { + //根据id查询 + SysUser sysUser = this.getById(id); + //账号头像存储地址 + String imgPath = fileSpaceProperties.getSystem() + "user"; + String avatar = sysUser.getAvatar(); + if (StrUtil.isNotBlank(avatar)) { + String imgName = imgPath + File.separator + avatar; + FileUtil.del(imgName); + } + //上传图片 并获取图片名称 (图片格式修改成png) + String imgName = FileUtil.upload(img, imgPath, + IdUtil.fastSimpleUUID() + "." + FileUtil.getExtensionName(img.getOriginalFilename())).getName(); + //修改 账户头像 + sysUser.setAvatar(imgName); + //修改 最近修改者 + sysUser.setLastmodifier(getUsername()); + //修改 最近修改日期 + sysUser.setLastmodifydate(new Timestamp(System.currentTimeMillis())); + //更新用户表 + boolean isOk = this.updateById(sysUser); + return isOk; + } + + /*********************************** + * 用途说明:新增系统角色用户对照表 对用户分配角色(单个) + * 参数说明 + * id 生成的id + * roleid 角色id + * userid 用户id + * 返回值说明: + ************************************/ + @Override + public boolean addUserRoles(String roleid, String userid) { + boolean isOk = true; + //根据角色id与用户id查询 + List list = sysUserMapper.getRoleUsersByid(roleid, userid); + //判断是否用户已分配此权限 + if (list.size() == 0) { + //系统生成id + String id = IdUtil.fastSimpleUUID(); + //新增sys_role_users表数据 + isOk = sysUserMapper.addUserRoles(id, roleid, userid); + } + return isOk; + } + + /* @Override + public Page queryUsers(String orgid, + String username, Page page) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + // 分页查询中的条件查询 + if (StrUtil.isNotBlank(username)) { + queryWrapper.like(SysUser::getUsername, username); + } + queryWrapper.eq(SysUser::getOrgid, orgid); + return sysUserMapper.selectPage(page, queryWrapper); + }*/ + + @Override + public Page queryUsers(String orgid, + String username, + Page page) { + Page mapPage = sysUserMapper.queryUsers(orgid, + username, page); + ;mapPage.getRecords().forEach(record -> { + String id = record.getId(); + List sysRoles = sysRoleMapper.getRoleByUserId(id); + record.setRoles(sysRoles); + }); + return mapPage; + } + + /*********************************** + * 用途说明:根据ID批量删除用户 + * 参数说明 + *ids 用户id集合 + * 返回值说明: 判断是否删除成功 + ************************************/ + @Override + public boolean deleteUserByIds(String id) { + String[] splitId = id.split(","); + List ids = Arrays.asList(splitId); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(SysUser::getId, ids); + List sysUsers = sysUserMapper.selectList(queryWrapper); + List names = + sysUsers.stream().map(SysUser::getUsername).collect(Collectors.toList()); + if (names.contains("admin")) { + return false; + } else { + int result = sysUserMapper.deleteBatchIds(ids); + if (result <= 0) { + return false; + } + // 根据ID删除用户与角色的关联信息 + sysUserMapper.delRoleUsersByUserIds(ids); + List avatars = + sysUsers.stream().map(SysUser::getAvatar).collect(Collectors.toList()); + if (avatars.size() > 0) { + for (String avatar : avatars) { + //账号头像存储地址 + String imgName = + fileSpaceProperties.getSystem() + File.separator + "user" + File.separator + avatar; + FileUtil.del(imgName); + } + } + return true; + } + } + + /*********************************** + * 用途说明:比较登录名称是否有重复 + * 参数说明 + * account 登录名称 + * 返回值说明: 重复返回 false 否则返回 true + ************************************/ + private boolean isExistAccount(String username) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (this.list(queryWrapper.eq("username", username)).size() > 0) { + //判断 查询登录账号 结果集是否为null 重复返回 false 否则返回 tree + return false; + } else { + return true; + } + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/task/TaskMessage.java b/backend/platform-system/src/main/java/com/yfd/platform/system/task/TaskMessage.java new file mode 100644 index 0000000..45f5e46 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/task/TaskMessage.java @@ -0,0 +1,57 @@ +package com.yfd.platform.system.task; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.yfd.platform.component.ServerSendEventServer; +import com.yfd.platform.config.MessageConfig; +import com.yfd.platform.config.WebConfig; +import com.yfd.platform.system.domain.Message; +import com.yfd.platform.system.service.IMessageService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import java.sql.Timestamp; +import java.util.List; + +/** + * @author TangWei + * @Date: 2023/3/22 15:39 + * @Description: + */ +@Component +public class TaskMessage { + + @Resource + private IMessageService messageService; + + /********************************** + * 用途说明: 定时监控消息是否过期 + * 参数说明 + * 返回值说明: void + ***********************************/ + public void examineMessage() { + List list = + messageService.list(new LambdaQueryWrapper().eq(Message::getStatus, "1")); + for (Message message : list) { + Timestamp createtime = message.getCreatetime(); + Timestamp timestamp = new Timestamp(System.currentTimeMillis()); + long create = createtime.getTime(); + long now = timestamp.getTime(); + Integer validperiod = message.getValidperiod(); + long v = validperiod * 60 * 60 * 1000; + if ((now - create) > v) { + message.setStatus("9"); + message.setReadtime(timestamp); + messageService.updateById(message); + } + + } + } + + /*public void sendMessage() { + String loginToken = webConfig.loginuserCache().get("loginToken"); + long count = + messageService.count(new LambdaQueryWrapper().eq(Message::getStatus, "1")); + ServerSendEventServer.sendMessage(loginToken, count + ""); + }*/ +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/utils/CallBack.java b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/CallBack.java new file mode 100644 index 0000000..e1f9bc2 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/CallBack.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.yfd.platform.system.utils; + +/** + * @author: liaojinlong + * @date: 2020/6/9 17:02 + * @since: 1.0 + * @see {@link SpringContextHolder} + * 针对某些初始化方法,在SpringContextHolder 初始化前时,
+ * 可提交一个 提交回调任务。
+ * 在SpringContextHolder 初始化后,进行回调使用 + */ + +public interface CallBack { + /** + * 回调执行方法 + */ + void executor(); + + /** + * 本回调任务名称 + * @return / + */ + default String getCallBackName() { + return Thread.currentThread().getId() + ":" + this.getClass().getName(); + } +} + diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/utils/CodeGenerator.java b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/CodeGenerator.java new file mode 100644 index 0000000..6674482 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/CodeGenerator.java @@ -0,0 +1,75 @@ +package com.yfd.platform.system.utils; + +import com.baomidou.mybatisplus.generator.FastAutoGenerator; +import com.baomidou.mybatisplus.generator.config.OutputFile; +import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; +import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine; + +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; + +// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中 +public class CodeGenerator { + + public static String scanner(String tip) { + Scanner scanner = new Scanner(System.in); + StringBuilder help = new StringBuilder(); + help.append("请输入" + tip + ":"); + System.out.println(help.toString()); + if (scanner.hasNext()) { + String ipt = scanner.next(); + if (ipt != null && !ipt.trim().isEmpty()) { + return ipt; + } + } + throw new RuntimeException("请输入正确的" + tip + "!"); + } + + public static void main(String[] args) { + String projectPath = System.getProperty("user.dir"); + String module = scanner("模块名称"); + + Map pathInfo = new HashMap<>(); + pathInfo.put(OutputFile.entity, projectPath + "/src/main/java/com/yfd/platform/" + module + "/domain"); + pathInfo.put(OutputFile.mapper, projectPath + "/src/main/java/com/yfd/platform/" + module + "/mapper"); + pathInfo.put(OutputFile.controller, projectPath + "/src/main/java/com/yfd/platform/" + module + "/controller"); + pathInfo.put(OutputFile.serviceImpl, projectPath + "/src/main/java/com/yfd/platform/" + module + "/service/impl"); + pathInfo.put(OutputFile.service, projectPath + "/src/main/java/com/yfd/platform/" + module + "/service"); + pathInfo.put(OutputFile.xml, projectPath + "/src/main/resources/mapper/" + module); + + FastAutoGenerator.create( + "jdbc:mysql://43.143.220.7:3306/frameworkdb2023?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&autoReconnect=true&failOverReadOnly=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai", + "root", + "zhengg7QkXa { + builder.author("TangWei") + .disableOpenDir() + .outputDir(projectPath + "/src/main/java"); + }) + .packageConfig(builder -> { + builder.parent("com.yfd.platform") + .moduleName(module) + .pathInfo(pathInfo); + }) + .strategyConfig(builder -> { + builder.addInclude(scanner("表名,多个英文逗号分割").split(",")) + .entityBuilder() + .enableLombok() + .naming(NamingStrategy.underline_to_camel) + .columnNaming(NamingStrategy.underline_to_camel) + .controllerBuilder() + .enableRestStyle() + .mapperBuilder() + .formatMapperFileName("%sMapper") + .serviceBuilder() + .formatServiceFileName("%sService") + .formatServiceImplFileName("%sServiceImpl") + .controllerBuilder() + .formatFileName("%sController"); + }) + .templateEngine(new FreemarkerTemplateEngine()) + .execute(); + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/utils/EncryptConfigUtil.java b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/EncryptConfigUtil.java new file mode 100644 index 0000000..d678d70 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/EncryptConfigUtil.java @@ -0,0 +1,28 @@ +package com.yfd.platform.system.utils; + +import org.jasypt.encryption.pbe.PooledPBEStringEncryptor; +import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig; +import org.jasypt.util.text.BasicTextEncryptor; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +public class EncryptConfigUtil { + + public static void main(String[] args) { + +// String salt = "rca20230101"; +// String password = "123456"; +// +// BasicTextEncryptor textEncryptor = new BasicTextEncryptor(); +// //加密所需的salt +// textEncryptor.setPassword(salt); +// //要加密的数据(数据库的用户名或密码) +// String encrypt = textEncryptor.encrypt(password); +// System.out.println("password:"+encrypt); + + + BCryptPasswordEncoder passwordEncoder=new BCryptPasswordEncoder(); + String cryptPassword=passwordEncoder.encode("dl_2023");//设置缺省密码 + + } + +} \ No newline at end of file diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/utils/EncryptUtils.java b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/EncryptUtils.java new file mode 100644 index 0000000..a31c333 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/EncryptUtils.java @@ -0,0 +1,100 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.utils; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; +import javax.crypto.spec.IvParameterSpec; +import java.nio.charset.StandardCharsets; + +/** + * 加密 + * @author + * @date 2018-11-23 + */ + +public class EncryptUtils { + + private static final String STR_PARAM = "Passw0rd"; + + private static Cipher cipher; + + private static final IvParameterSpec IV = new IvParameterSpec(STR_PARAM.getBytes(StandardCharsets.UTF_8)); + + private static DESKeySpec getDesKeySpec(String source) throws Exception { + if (source == null || source.length() == 0){ + return null; + } + cipher = Cipher.getInstance("DES/CBC/PKCS5Padding"); + String strKey = "Passw0rd"; + return new DESKeySpec(strKey.getBytes(StandardCharsets.UTF_8)); + } + + /** + * 对称加密 + */ + public static String desEncrypt(String source) throws Exception { + DESKeySpec desKeySpec = getDesKeySpec(source); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); + SecretKey secretKey = keyFactory.generateSecret(desKeySpec); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, IV); + return byte2hex( + cipher.doFinal(source.getBytes(StandardCharsets.UTF_8))).toUpperCase(); + } + + /** + * 对称解密 + */ + public static String desDecrypt(String source) throws Exception { + byte[] src = hex2byte(source.getBytes(StandardCharsets.UTF_8)); + DESKeySpec desKeySpec = getDesKeySpec(source); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); + SecretKey secretKey = keyFactory.generateSecret(desKeySpec); + cipher.init(Cipher.DECRYPT_MODE, secretKey, IV); + byte[] retByte = cipher.doFinal(src); + return new String(retByte); + } + + private static String byte2hex(byte[] inStr) { + String stmp; + StringBuilder out = new StringBuilder(inStr.length * 2); + for (byte b : inStr) { + stmp = Integer.toHexString(b & 0xFF); + if (stmp.length() == 1) { + // 如果是0至F的单位字符串,则添加0 + out.append("0").append(stmp); + } else { + out.append(stmp); + } + } + return out.toString(); + } + + private static byte[] hex2byte(byte[] b) { + int size = 2; + if ((b.length % size) != 0){ + throw new IllegalArgumentException("长度不是偶数"); + } + byte[] b2 = new byte[b.length / 2]; + for (int n = 0; n < b.length; n += size) { + String item = new String(b, n, 2); + b2[n / 2] = (byte) Integer.parseInt(item, 16); + } + return b2; + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/utils/ExecutionJob.java b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/ExecutionJob.java new file mode 100644 index 0000000..5124b7a --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/ExecutionJob.java @@ -0,0 +1,103 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.utils; + +import com.yfd.platform.config.MessageConfig; +import com.yfd.platform.config.thread.ThreadPoolExecutorUtil; +import com.yfd.platform.system.domain.Message; +import com.yfd.platform.system.domain.QuartzJob; +import com.yfd.platform.system.service.IMessageService; +import com.yfd.platform.system.service.IQuartzJobService; +import org.quartz.JobExecutionContext; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.quartz.QuartzJobBean; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import java.sql.Timestamp; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 参考人人开源,https://gitee.com/renrenio/renren-security + * + * @author / + * @date 2019-01-07 + */ +@Async +@SuppressWarnings({"unchecked", "all"}) +public class ExecutionJob extends QuartzJobBean { + + /** + * 该处仅供参考 + */ + private final static ThreadPoolExecutor EXECUTOR = + ThreadPoolExecutorUtil.getPoll(); + + @Resource + private IMessageService messageService; + + @Resource + private MessageConfig messageConfig; + + @Override + public void executeInternal(JobExecutionContext context) { + QuartzJob quartzJob = + (QuartzJob) context.getMergedJobDataMap().get(QuartzJob.JOB_KEY); + // 获取spring bean + IQuartzJobService quartzJobService = + SpringContextHolder.getBean(IQuartzJobService.class); + String uuid = quartzJob.getId(); + long startTime = System.currentTimeMillis(); + String jobName = quartzJob.getJobName(); + try { + // 执行任务 + System.out.println( + "--------------------------------------------------------------"); + System.out.println("任务开始执行,任务名称:" + jobName); + QuartzRunnable task = new QuartzRunnable(quartzJob.getJobClass(), + quartzJob.getJobMethod(), + quartzJob.getJobParams()); + Future future = EXECUTOR.submit(task); + future.get(); + long times = System.currentTimeMillis() - startTime; + Message message = new Message(); + message.setCreatetime(new Timestamp(System.currentTimeMillis())); + message.setType("1"); + message.setTitle(quartzJob.getJobName()); + message.setContent(quartzJob.getDescription()); + message.setSenderName("定时器"); + message.setReceiverCodes(quartzJob.getOrderno().toString()); + message.setReceiverNames(""); + message.setStatus("1"); + message.setValidperiod(24); + messageConfig.addMessage(message); + // 任务状态 + System.out.println("任务执行完毕,任务名称:" + jobName + ", " + + "执行时间:" + times + "毫秒"); + System.out.println( + "--------------------------------------------------------------"); + } catch (Exception e) { + System.out.println("任务执行失败,任务名称:" + jobName); + System.out.println( + "--------------------------------------------------------------"); + quartzJob.setStatus("0"); + //更新状态 + quartzJobService.updateById(quartzJob); + } + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/utils/FileUtil.java b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/FileUtil.java new file mode 100644 index 0000000..9873fe4 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/FileUtil.java @@ -0,0 +1,398 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.utils; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.poi.excel.BigExcelWriter; +import cn.hutool.poi.excel.ExcelUtil; +import com.yfd.platform.exception.BadRequestException; +import org.apache.commons.lang3.StringUtils; +import org.apache.poi.util.IOUtils; +import org.apache.poi.xssf.streaming.SXSSFSheet; +import org.apache.tomcat.util.http.fileupload.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.*; +import java.net.URLDecoder; +import java.security.MessageDigest; +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * File工具类,扩展 hutool 工具包 + * + * @author + * @date 2018-12-27 + */ +public class FileUtil extends cn.hutool.core.io.FileUtil { + + private static final Logger log = LoggerFactory.getLogger(FileUtil.class); + + /** + * 系统临时目录 + *
+ * windows 包含路径分割符,但Linux 不包含, + * 在windows \\==\ 前提下, + * 为安全起见 同意拼装 路径分割符, + *
+     *       java.io.tmpdir
+     *       windows : C:\Users/xxx\AppData\Local\Temp\
+     *       linux: /temp
+     * 
+ */ + public static final String SYS_TEM_DIR = System.getProperty("java.io.tmpdir") + File.separator; + /** + * 定义GB的计算常量 + */ + private static final int GB = 1024 * 1024 * 1024; + /** + * 定义MB的计算常量 + */ + private static final int MB = 1024 * 1024; + /** + * 定义KB的计算常量 + */ + private static final int KB = 1024; + + /** + * 格式化小数 + */ + private static final DecimalFormat DF = new DecimalFormat("0.00"); + + public static final String IMAGE = "image"; + public static final String TXT = "document"; + public static final String MUSIC = "music"; + public static final String VIDEO = "video"; + public static final String OTHER = "other"; + + + /** + * MultipartFile转File + */ + public static File toFile(MultipartFile multipartFile) { + // 获取文件名 + String fileName = multipartFile.getOriginalFilename(); + // 获取文件后缀 + String prefix = "." + getExtensionName(fileName); + File file = null; + try { + // 用uuid作为文件名,防止生成的临时文件重复 + file = File.createTempFile(IdUtil.simpleUUID(), prefix); + // MultipartFile to File + multipartFile.transferTo(file); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + return file; + } + + /** + * 获取文件扩展名,不带 . + */ + public static String getExtensionName(String filename) { + if ((filename != null) && (filename.length() > 0)) { + int dot = filename.lastIndexOf('.'); + if ((dot > -1) && (dot < (filename.length() - 1))) { + return filename.substring(dot + 1); + } + } + return filename; + } + + /** + * Java文件操作 获取不带扩展名的文件名 + */ + public static String getFileNameNoEx(String filename) { + if ((filename != null) && (filename.length() > 0)) { + int dot = filename.lastIndexOf('.'); + if ((dot > -1) && (dot < (filename.length()))) { + return filename.substring(0, dot); + } + } + return filename; + } + + /** + * 文件大小转换 + */ + public static String getSize(long size) { + String resultSize; + if (size / GB >= 1) { + //如果当前Byte的值大于等于1GB + resultSize = DF.format(size / (float) GB) + "GB "; + } else if (size / MB >= 1) { + //如果当前Byte的值大于等于1MB + resultSize = DF.format(size / (float) MB) + "MB "; + } else if (size / KB >= 1) { + //如果当前Byte的值大于等于1KB + resultSize = DF.format(size / (float) KB) + "KB "; + } else { + resultSize = size + "B "; + } + return resultSize; + } + + /** + * inputStream 转 File + */ + static File inputStreamToFile(InputStream ins, String name) throws Exception { + File file = new File(SYS_TEM_DIR + name); + if (file.exists()) { + return file; + } + OutputStream os = new FileOutputStream(file); + int bytesRead; + int len = 8192; + byte[] buffer = new byte[len]; + while ((bytesRead = ins.read(buffer, 0, len)) != -1) { + os.write(buffer, 0, bytesRead); + } + os.close(); + ins.close(); + return file; + } + + /** + * 将文件名解析成文件的上传路径 + */ + public static File upload(MultipartFile file, String filePath) { + Date date = new Date(); + SimpleDateFormat format = new SimpleDateFormat("yyyyMMddhhmmssS"); + String name = getFileNameNoEx(file.getOriginalFilename()); + String suffix = getExtensionName(file.getOriginalFilename()); + String nowStr = "-" + format.format(date); + try { + String fileName = name + "." + suffix; + String path = filePath +File.separator + fileName; + // getCanonicalFile 可解析正确各种路径 + File dest = new File(path).getCanonicalFile(); + // 检测是否存在目录 + if (!dest.getParentFile().exists()) { + if (!dest.getParentFile().mkdirs()) { + System.out.println("was not successful."); + } + } + // 文件写入 + file.transferTo(dest); + return dest; + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + /** + * 将文件名解析成文件的上传路径 + * file 上传的文件 + * filePath 存储路径 + * tofilename 保存文件名称 + + */ + public static File upload(MultipartFile file, String filePath,String tofilename) { + try { + String filename = filePath + File.separator + tofilename; + File dest = new File(filename).getCanonicalFile(); + // 检测是否存在目录 + if (!dest.getParentFile().exists()) { + if (!dest.getParentFile().mkdirs()) { + } + } + // 文件写入 + file.transferTo(dest); + return dest; + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + + /** + * 导出excel + */ + public static void downloadExcel(List> list, HttpServletResponse response) throws IOException { + String tempPath = SYS_TEM_DIR + IdUtil.fastSimpleUUID() + ".xlsx"; + String filename = "record"+cn.hutool.core.date.DateUtil.format(DateUtil.date(), "yyyyMMddHHmmss"); + File file = new File(tempPath); + BigExcelWriter writer = ExcelUtil.getBigWriter(file); + // 一次性写出内容,使用默认样式,强制输出标题 + writer.write(list, true); + SXSSFSheet sheet = (SXSSFSheet)writer.getSheet(); + //上面需要强转SXSSFSheet 不然没有trackAllColumnsForAutoSizing方法 + sheet.trackAllColumnsForAutoSizing(); + //列宽自适应 + writer.autoSizeColumnAll(); + //response为HttpServletResponse对象 + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"); + //test.xls是弹出下载对话框的文件名,不能为中文,中文请自行编码 + response.setHeader("Content-Disposition", "attachment;filename="+filename+".xlsx"); + ServletOutputStream out = response.getOutputStream(); + // 终止后删除临时文件 + file.deleteOnExit(); + writer.flush(out, true); + //此处记得关闭输出Servlet流 + IoUtil.close(out); + } + + public static String getFileType(String type) { + String documents = "txt doc pdf ppt pps xlsx xls docx"; + String music = "mp3 wav wma mpa ram ra aac aif m4a"; + String video = "avi mpg mpe mpeg asf wmv mov qt rm mp4 flv m4v webm ogv ogg"; + String image = "bmp dib pcp dif wmf gif jpg tif eps psd cdr iff tga pcd mpt png jpeg"; + if (image.contains(type)) { + return IMAGE; + } else if (documents.contains(type)) { + return TXT; + } else if (music.contains(type)) { + return MUSIC; + } else if (video.contains(type)) { + return VIDEO; + } else { + return OTHER; + } + } + + public static void checkSize(long maxSize, long size) { + // 1M + int len = 1024 * 1024; + if (size > (maxSize * len)) { + throw new BadRequestException("文件超出规定大小"); + } + } + + /** + * 判断两个文件是否相同 + */ + public static boolean check(File file1, File file2) { + String img1Md5 = getMd5(file1); + String img2Md5 = getMd5(file2); + return img1Md5.equals(img2Md5); + } + + /** + * 判断两个文件是否相同 + */ + public static boolean check(String file1Md5, String file2Md5) { + return file1Md5.equals(file2Md5); + } + + private static byte[] getByte(File file) { + // 得到文件长度 + byte[] b = new byte[(int) file.length()]; + try { + InputStream in = new FileInputStream(file); + try { + System.out.println(in.read(b)); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } catch (FileNotFoundException e) { + log.error(e.getMessage(), e); + return null; + } + return b; + } + + private static String getMd5(byte[] bytes) { + // 16进制字符 + char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + try { + MessageDigest mdTemp = MessageDigest.getInstance("MD5"); + mdTemp.update(bytes); + byte[] md = mdTemp.digest(); + int j = md.length; + char[] str = new char[j * 2]; + int k = 0; + // 移位 输出字符串 + for (byte byte0 : md) { + str[k++] = hexDigits[byte0 >>> 4 & 0xf]; + str[k++] = hexDigits[byte0 & 0xf]; + } + return new String(str); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + /** + * 下载文件 + * + * @param request / + * @param response / + * @param file / + */ + public static void downloadFile(HttpServletRequest request, HttpServletResponse response, File file, boolean deleteOnExit) { + response.setCharacterEncoding(request.getCharacterEncoding()); + response.setContentType("application/octet-stream"); + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + response.setHeader("Content-Disposition", "attachment; filename=" + file.getName()); + IOUtils.copy(fis, response.getOutputStream()); + response.flushBuffer(); + } catch (Exception e) { + log.error(e.getMessage(), e); + } finally { + if (fis != null) { + try { + fis.close(); + if (deleteOnExit) { + file.deleteOnExit(); + } + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + } + } + /** + * 预览PDF文件 + * + * @param filepath / + * @param response / + */ + public static void viewPDF(String filepath, HttpServletResponse response) throws IOException { + File file=new File(filepath); + String originFileName=file.getName(); //中文编码 + response.setCharacterEncoding("UTF-8"); + String showName= StrUtil.isNotBlank(originFileName)?originFileName:file.getName(); + showName= URLDecoder.decode(showName,"UTF-8"); + response.setHeader("Content-Disposition","inline;fileName="+new String(showName.getBytes(), "ISO8859-1")+";fileName*=UTF-8''"+ new String(showName.getBytes(), "ISO8859-1")); + FileInputStream fis = new FileInputStream(file); + response.setHeader("content-type", "application/pdf"); + response.setContentType("application/pdf; charset=utf-8"); + IOUtils.copy(fis, response.getOutputStream()); + fis.close(); + } + + public static String getMd5(File file) { + return getMd5(getByte(file)); + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/utils/MpGenerator.java b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/MpGenerator.java new file mode 100644 index 0000000..943e07e --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/MpGenerator.java @@ -0,0 +1,79 @@ +package com.yfd.platform.system.utils; + +import com.baomidou.mybatisplus.generator.FastAutoGenerator; +import com.baomidou.mybatisplus.generator.config.OutputFile; +import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; +import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine; +import org.springframework.util.StringUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; + +public class MpGenerator { + /** + * 读取控制台内容 + */ + public static String scanner(String tip) { + Scanner scanner = new Scanner(System.in); + StringBuilder help = new StringBuilder(); + help.append("请输入" + tip + ":"); + System.out.println(help.toString()); + if (scanner.hasNext()) { + String ipt = scanner.next(); + if (StringUtils.hasText(ipt)) { + return ipt; + } + } + throw new RuntimeException("请输入正确的" + tip + "!"); + } + + public static void main(String[] args) { + String projectPath = System.getProperty("user.dir"); + String url = PropertiesUtils.getPropertyField("spring.datasource.url"); + String username = PropertiesUtils.getPropertyField("spring.datasource.username"); + String password = PropertiesUtils.getPropertyField("spring.datasource.password"); + + String moduleName = scanner("模块名"); + String modulePath = moduleName.replace(".", "/"); + + Map pathInfo = new HashMap<>(); + pathInfo.put(OutputFile.entity, projectPath + "/src/main/java/com/yfd/platform/modules/domain" + modulePath + "/entity"); + pathInfo.put(OutputFile.mapper, projectPath + "/src/main/java/com/yfd/platform/modules/domain/" + modulePath + "/dao"); + pathInfo.put(OutputFile.controller, projectPath + "/src/main/java/com/yfd/platform/modules/domain" + modulePath + "/controller"); + pathInfo.put(OutputFile.service, projectPath + "/src/main/java/com/yfd/platform/modules/domain" + modulePath + "/service"); + pathInfo.put(OutputFile.serviceImpl, projectPath + "/src/main/java/com/yfd/platform/modules/domain" + modulePath + "/service/impl"); + pathInfo.put(OutputFile.xml, projectPath + "/src/main/resources/mapper/" + modulePath); + + FastAutoGenerator.create(url, username, password) + .globalConfig(builder -> { + builder.author("fwh") + .disableOpenDir() + .outputDir(projectPath + "/src/main/java"); + }) + .packageConfig(builder -> { + builder.parent(PropertiesUtils.getPropertyField("project.package.name")) + .moduleName(moduleName) + .pathInfo(pathInfo); + }) + .strategyConfig(builder -> { + builder.addInclude(scanner("表名,多个英文逗号分割").split(",")) + .entityBuilder() + .enableLombok() + .naming(NamingStrategy.underline_to_camel) + .columnNaming(NamingStrategy.underline_to_camel) + .controllerBuilder() + .enableRestStyle() + .mapperBuilder() + .formatMapperFileName("%sDao") + .serviceBuilder() + .formatServiceFileName("%sService") + .formatServiceImplFileName("%sServiceImpl") + .controllerBuilder() + .formatFileName("%sController"); + }) + .templateEngine(new FreemarkerTemplateEngine()) + .execute(); + } +} + diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/utils/ObjectConverterUtil.java b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/ObjectConverterUtil.java new file mode 100644 index 0000000..d29488e --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/ObjectConverterUtil.java @@ -0,0 +1,149 @@ +package com.yfd.platform.system.utils; + +import java.lang.reflect.Field; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +public class ObjectConverterUtil { + + // 日期时间格式 + private static final DateTimeFormatter DATE_TIME_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 将 List 中的大写字段名转换为与实体类一致的小写格式 + * 适用于 Oracle 数据库查询结果转换 + * 时间类型字段会转换为字符串格式 + * + * @param entityClass 实体类 Class 对象 + * @param sourceList 源数据列表(字段名为大写) + * @return 转换后的列表(字段名与实体类一致) + */ + public static List> convertMapFieldsToEntityFormat( + Class entityClass, + List> sourceList) { + + if (sourceList == null || sourceList.isEmpty()) { + return sourceList; + } + + // 获取实体类的所有字段及类型 + Map fieldMap = new HashMap<>(); + for (Field field : entityClass.getDeclaredFields()) { + fieldMap.put(field.getName(), field); + } + + // 转换每个 Map + return sourceList.stream().map(sourceMap -> { + Map targetMap = new HashMap<>(); + + for (Map.Entry entry : sourceMap.entrySet()) { + String dbColumnName = entry.getKey(); // 数据库列名(大写) + Object value = entry.getValue(); + + // 将大写列名转为小写 + String lowerCaseName = dbColumnName.toLowerCase(); + + // 如果实体类中有对应的字段 + if (fieldMap.containsKey(lowerCaseName)) { + Field field = fieldMap.get(lowerCaseName); + String fieldType = field.getType().getSimpleName(); + + // 处理时间类型,转换为字符串 + if ("Timestamp".equals(fieldType) || "Date".equals(fieldType) || + "LocalDateTime".equals(fieldType)) { + targetMap.put(lowerCaseName, formatDateTimeValue(value)); + } else { + targetMap.put(lowerCaseName, value); + } + } else { + // 如果实体类中没有对应字段,保留原列名(转小写) + targetMap.put(lowerCaseName, value); + } + } + + return targetMap; + }).collect(Collectors.toList()); + } + + /** + * 通用方法:将单个 Map 的大写字段转换为实体类格式 + * 时间类型字段会转换为字符串格式 + * + * @param entityClass 实体类 Class 对象 + * @param sourceMap 源 Map(字段名为大写) + * @return 转换后的 Map(字段名与实体类一致) + */ + public static Map convertSingleMapFieldsToEntityFormat( + Class entityClass, + Map sourceMap) { + + if (sourceMap == null) { + return sourceMap; + } + + // 获取实体类的所有字段及类型 + Map fieldMap = new HashMap<>(); + for (Field field : entityClass.getDeclaredFields()) { + fieldMap.put(field.getName(), field); + } + + Map targetMap = new HashMap<>(); + + for (Map.Entry entry : sourceMap.entrySet()) { + String dbColumnName = entry.getKey(); + Object value = entry.getValue(); + + // 将大写列名转为小写 + String lowerCaseName = dbColumnName.toLowerCase(); + + // 验证字段是否存在于实体类中 + if (fieldMap.containsKey(lowerCaseName)) { + Field field = fieldMap.get(lowerCaseName); + String fieldType = field.getType().getSimpleName(); + + // 处理时间类型,转换为字符串 + if ("Timestamp".equals(fieldType) || "Date".equals(fieldType) || + "LocalDateTime".equals(fieldType)) { + targetMap.put(lowerCaseName, formatDateTimeValue(value)); + } else { + targetMap.put(lowerCaseName, value); + } + } + } + + return targetMap; + } + + /** + * 格式化时间类型值为字符串 + * + * @param value 时间值(可能是 Timestamp、Date 或 LocalDateTime) + * @return 格式化后的字符串,如果为 null 则返回 null + */ + private static String formatDateTimeValue(Object value) { + if (value == null) { + return null; + } + + if (value instanceof Timestamp) { + LocalDateTime localDateTime = ((Timestamp) value).toLocalDateTime(); + return localDateTime.format(DATE_TIME_FORMATTER); + } else if (value instanceof Date) { + // 包括 java.sql.Date 和 java.util.Date + LocalDateTime localDateTime = ((Date) value).toInstant() + .atZone(java.time.ZoneId.systemDefault()) + .toLocalDateTime(); + return localDateTime.format(DATE_TIME_FORMATTER); + } else if (value instanceof LocalDateTime) { + return ((LocalDateTime) value).format(DATE_TIME_FORMATTER); + } else { + // 其他类型直接转字符串 + return value.toString(); + } + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/utils/PropertiesUtils.java b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/PropertiesUtils.java new file mode 100644 index 0000000..9ba8618 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/PropertiesUtils.java @@ -0,0 +1,29 @@ +package com.yfd.platform.system.utils; + +import org.springframework.core.io.ClassPathResource; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Properties; +/****************************** + * 用途说明: + * 作者姓名: pcj + * 创建时间: 2022/9/20 14:31 + ******************************/ +public class PropertiesUtils { + public final static String RESOURCE_PATH = "application.properties"; + + public final static Properties properties = new Properties(); + + public static String getPropertyField(String parameter) { + //对应resources目录下的资源路径 + ClassPathResource resource = new ClassPathResource(RESOURCE_PATH); + try { + properties.load(new InputStreamReader(resource.getInputStream(), "gbk")); + } catch (IOException e) { + throw new RuntimeException(e); + } + return properties.getProperty(parameter); + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/utils/QuartzManage.java b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/QuartzManage.java new file mode 100644 index 0000000..c4fc18f --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/QuartzManage.java @@ -0,0 +1,187 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.utils; + +import com.yfd.platform.system.domain.QuartzJob; +import lombok.extern.slf4j.Slf4j; +import org.quartz.*; +import org.quartz.impl.triggers.CronTriggerImpl; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import java.util.Date; + +import static org.quartz.TriggerBuilder.newTrigger; + +/** + * @author + * @date 2019-01-07 + */ +@Slf4j +@Component +public class QuartzManage { + + private static final String JOB_NAME = "TASK_"; + + @Resource(name = "scheduler") + private Scheduler scheduler; + + public void addJob(QuartzJob quartzJob) { + try { + // 构建job信息 + JobDetail jobDetail = JobBuilder.newJob(ExecutionJob.class). + withIdentity(JOB_NAME + quartzJob.getId()).build(); + + //通过触发器名和cron 表达式创建 Trigger + Trigger cronTrigger = newTrigger() + .withIdentity(JOB_NAME + quartzJob.getId()) + .startNow() + .withSchedule(CronScheduleBuilder.cronSchedule(quartzJob.getJobCron())) + .build(); + + cronTrigger.getJobDataMap().put(QuartzJob.JOB_KEY, quartzJob); + + //重置启动时间 + ((CronTriggerImpl) cronTrigger).setStartTime(new Date()); + + //执行定时任务 + scheduler.scheduleJob(jobDetail, cronTrigger); + + // 暂停任务 + if ("0".equals(quartzJob.getStatus())) { + pauseJob(quartzJob); + } + } catch (Exception e) { + log.error("创建定时任务失败", e); + throw new RuntimeException("创建定时任务失败"); + } + } + + /** + * 更新job cron表达式 + * + * @param quartzJob / + */ + public void updateJobCron(QuartzJob quartzJob) { + try { + TriggerKey triggerKey = + TriggerKey.triggerKey(JOB_NAME + quartzJob.getId()); + CronTrigger trigger = + (CronTrigger) scheduler.getTrigger(triggerKey); + // 如果不存在则创建一个定时任务 + if (trigger == null) { + addJob(quartzJob); + trigger = (CronTrigger) scheduler.getTrigger(triggerKey); + } + CronScheduleBuilder scheduleBuilder = + CronScheduleBuilder.cronSchedule(quartzJob.getJobCron()); + trigger = + trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build(); + //重置启动时间 + ((CronTriggerImpl) trigger).setStartTime(new Date()); + trigger.getJobDataMap().put(QuartzJob.JOB_KEY, quartzJob); + + scheduler.rescheduleJob(triggerKey, trigger); + // 暂停任务 + if ("0".equals(quartzJob.getStatus())) { + pauseJob(quartzJob); + } + } catch (Exception e) { + log.error("更新定时任务失败", e); + throw new RuntimeException("更新定时任务失败"); + } + + } + + /** + * 删除一个job + * + * @param quartzJob / + */ + public void deleteJob(QuartzJob quartzJob) { + try { + JobKey jobKey = JobKey.jobKey(JOB_NAME + quartzJob.getId()); + scheduler.pauseJob(jobKey); + scheduler.deleteJob(jobKey); + } catch (Exception e) { + log.error("删除定时任务失败", e); + throw new RuntimeException("删除定时任务失败"); + } + } + + /** + * 恢复一个job + * + * @param quartzJob / + */ + public void resumeJob(QuartzJob quartzJob) { + try { + TriggerKey triggerKey = + TriggerKey.triggerKey(JOB_NAME + quartzJob.getId()); + CronTrigger trigger = + (CronTrigger) scheduler.getTrigger(triggerKey); + // 如果不存在则创建一个定时任务 + if (trigger == null) { + addJob(quartzJob); + } + JobKey jobKey = JobKey.jobKey(JOB_NAME + quartzJob.getId()); + scheduler.resumeJob(jobKey); + } catch (Exception e) { + log.error("恢复定时任务失败", e); + throw new RuntimeException("恢复定时任务失败"); + } + } + + /** + * 立即执行job + * + * @param quartzJob / + */ + public void runJobNow(QuartzJob quartzJob) { + try { + TriggerKey triggerKey = + TriggerKey.triggerKey(JOB_NAME + quartzJob.getId()); + CronTrigger trigger = + (CronTrigger) scheduler.getTrigger(triggerKey); + // 如果不存在则创建一个定时任务 + if (trigger == null) { + addJob(quartzJob); + } + JobDataMap dataMap = new JobDataMap(); + dataMap.put(QuartzJob.JOB_KEY, quartzJob); + JobKey jobKey = JobKey.jobKey(JOB_NAME + quartzJob.getId()); + scheduler.triggerJob(jobKey, dataMap); + } catch (Exception e) { + log.error("定时任务执行失败", e); + throw new RuntimeException("定时任务执行失败"); + } + } + + /** + * 暂停一个job + * + * @param quartzJob / + */ + public void pauseJob(QuartzJob quartzJob) { + try { + JobKey jobKey = JobKey.jobKey(JOB_NAME + quartzJob.getId()); + scheduler.pauseJob(jobKey); + } catch (Exception e) { + log.error("定时任务暂停失败", e); + throw new RuntimeException("定时任务暂停失败"); + } + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/utils/QuartzRunnable.java b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/QuartzRunnable.java new file mode 100644 index 0000000..9d089c5 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/QuartzRunnable.java @@ -0,0 +1,58 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.utils; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Method; +import java.util.concurrent.Callable; + +/** + * 执行定时任务 + * @author / + */ +@Slf4j +public class QuartzRunnable implements Callable { + + private final Object target; + private final Method method; + private final String params; + + QuartzRunnable(String beanName, String methodName, String params) + throws NoSuchMethodException, SecurityException { + this.target = SpringContextHolder.getBean(beanName); + this.params = params; + + if (StringUtils.isNotBlank(params)) { + this.method = target.getClass().getDeclaredMethod(methodName, String.class); + } else { + this.method = target.getClass().getDeclaredMethod(methodName); + } + } + + @Override + public Object call() throws Exception { + ReflectionUtils.makeAccessible(method); + if (StringUtils.isNotBlank(params)) { + method.invoke(target, params); + } else { + method.invoke(target); + } + return null; + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/utils/RequestHolder.java b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/RequestHolder.java new file mode 100644 index 0000000..214189a --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/RequestHolder.java @@ -0,0 +1,34 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.utils; + +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Objects; + +/** + * 获取 HttpServletRequest + * @author + * @date 2018-11-24 + */ +public class RequestHolder { + + public static HttpServletRequest getHttpServletRequest() { + return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/utils/RsaUtils.java b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/RsaUtils.java new file mode 100644 index 0000000..4c9505b --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/RsaUtils.java @@ -0,0 +1,190 @@ +package com.yfd.platform.system.utils; + +import org.apache.commons.codec.binary.Base64; + +import javax.crypto.Cipher; +import java.security.*; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +/** + * @author https://www.cnblogs.com/nihaorz/p/10690643.html + * @description Rsa 工具类,公钥私钥生成,加解密 + * @date 2020-05-18 + **/ +public class RsaUtils { + + private static final String SRC = "123456"; + + public static void main(String[] args) throws Exception { + System.out.println("\n"); +// RsaKeyPair keyPair = generateKeyPair(); + String publicKey = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANL378k3RiZHWx5AfJqdH9xRNBmD9wGD2iRe41HdTNF8RUhNnHit5NpMNtGL0NPTSSpPjjI1kJfVorRvaQerUgkCAwEAAQ=="; + String privateKey = "MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEA0vfvyTdGJkdbHkB8\n" + + "mp0f3FE0GYP3AYPaJF7jUd1M0XxFSE2ceK3k2kw20YvQ09NJKk+OMjWQl9WitG9p\n" + + "B6tSCQIDAQABAkA2SimBrWC2/wvauBuYqjCFwLvYiRYqZKThUS3MZlebXJiLB+Ue\n" + + "/gUifAAKIg1avttUZsHBHrop4qfJCwAI0+YRAiEA+W3NK/RaXtnRqmoUUkb59zsZ\n" + + "UBLpvZgQPfj1MhyHDz0CIQDYhsAhPJ3mgS64NbUZmGWuuNKp5coY2GIj/zYDMJp6\n" + + "vQIgUueLFXv/eZ1ekgz2Oi67MNCk5jeTF2BurZqNLR3MSmUCIFT3Q6uHMtsB9Eha\n" + + "4u7hS31tj1UWE+D+ADzp59MGnoftAiBeHT7gDMuqeJHPL4b+kC+gzV4FGTfhR9q3\n" + + "tTbklZkD2A=="; + RsaKeyPair keyPair =new RsaKeyPair(publicKey, privateKey); + System.out.println("私钥:" + keyPair.getPrivateKey()); + System.out.println("\n"); + test1(keyPair); + System.out.println("\n"); +// test2(keyPair); + System.out.println("\n"); + } + + /** + * 公钥加密私钥解密 + */ + private static void test1(RsaKeyPair keyPair) throws Exception { + System.out.println("***************** 公钥加密私钥解密开始 *****************"); + String text1 = encryptByPublicKey(keyPair.getPublicKey(), RsaUtils.SRC); + String text2 = decryptByPrivateKey(keyPair.getPrivateKey(), text1); + System.out.println("加密前:" + RsaUtils.SRC); + System.out.println("加密后:" + text1); + System.out.println("解密后:" + text2); + if (RsaUtils.SRC.equals(text2)) { + System.out.println("解密字符串和原始字符串一致,解密成功"); + } else { + System.out.println("解密字符串和原始字符串不一致,解密失败"); + } + System.out.println("***************** 公钥加密私钥解密结束 *****************"); + } + + /** + * 私钥加密公钥解密 + * @throws Exception / + */ + private static void test2(RsaKeyPair keyPair) throws Exception { + System.out.println("***************** 私钥加密公钥解密开始 *****************"); + String text1 = encryptByPrivateKey(keyPair.getPrivateKey(), RsaUtils.SRC); + String text2 = decryptByPublicKey(keyPair.getPublicKey(), text1); + System.out.println("加密前:" + RsaUtils.SRC); + System.out.println("加密后:" + text1); + System.out.println("解密后:" + text2); + if (RsaUtils.SRC.equals(text2)) { + System.out.println("解密字符串和原始字符串一致,解密成功"); + } else { + System.out.println("解密字符串和原始字符串不一致,解密失败"); + } + System.out.println("***************** 私钥加密公钥解密结束 *****************"); + } + + /** + * 公钥解密 + * + * @param publicKeyText 公钥 + * @param text 待解密的信息 + * @return / + * @throws Exception / + */ + public static String decryptByPublicKey(String publicKeyText, String text) throws Exception { + X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyText)); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec); + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.DECRYPT_MODE, publicKey); + byte[] result = cipher.doFinal(Base64.decodeBase64(text)); + return new String(result); + } + + /** + * 私钥加密 + * + * @param privateKeyText 私钥 + * @param text 待加密的信息 + * @return / + * @throws Exception / + */ + public static String encryptByPrivateKey(String privateKeyText, String text) throws Exception { + PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyText)); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec); + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.ENCRYPT_MODE, privateKey); + byte[] result = cipher.doFinal(text.getBytes()); + return Base64.encodeBase64String(result); + } + + /** + * 私钥解密 + * + * @param privateKeyText 私钥 + * @param text 待解密的文本 + * @return / + * @throws Exception / + */ + public static String decryptByPrivateKey(String privateKeyText, String text) throws Exception { + PKCS8EncodedKeySpec pkcs8EncodedKeySpec5 = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyText)); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec5); + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + byte[] result = cipher.doFinal(Base64.decodeBase64(text)); + return new String(result); + } + + /** + * 公钥加密 + * + * @param publicKeyText 公钥 + * @param text 待加密的文本 + * @return / + */ + public static String encryptByPublicKey(String publicKeyText, String text) throws Exception { + X509EncodedKeySpec x509EncodedKeySpec2 = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyText)); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec2); + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + byte[] result = cipher.doFinal(text.getBytes()); + return Base64.encodeBase64String(result); + } + + /** + * 构建RSA密钥对 + * + * @return / + * @throws NoSuchAlgorithmException / + */ + public static RsaKeyPair generateKeyPair() throws NoSuchAlgorithmException { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(1024); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate(); + String publicKeyString = Base64.encodeBase64String(rsaPublicKey.getEncoded()); + String privateKeyString = Base64.encodeBase64String(rsaPrivateKey.getEncoded()); + return new RsaKeyPair(publicKeyString, privateKeyString); + } + + + /** + * RSA密钥对对象 + */ + public static class RsaKeyPair { + + private final String publicKey; + private final String privateKey; + + public RsaKeyPair(String publicKey, String privateKey) { + this.publicKey = publicKey; + this.privateKey = privateKey; + } + + public String getPublicKey() { + return publicKey; + } + + public String getPrivateKey() { + return privateKey; + } + + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/utils/SecurityUtils.java b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/SecurityUtils.java new file mode 100644 index 0000000..6708af1 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/SecurityUtils.java @@ -0,0 +1,84 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.utils; + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.yfd.platform.exception.BadRequestException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; + +import java.util.List; + +/** + * 获取当前登录的用户 + * @author + * @date 2019-01-17 + */ +@Slf4j +public class SecurityUtils { + + /** + * 获取当前登录的用户 + * @return UserDetails + */ + public static UserDetails getCurrentUser() { + UserDetailsService userDetailsService = SpringContextHolder.getBean(UserDetailsService.class); + return userDetailsService.loadUserByUsername(getCurrentUsername()); + } + + /** + * 获取系统用户名称 + * + * @return 系统用户名称 + */ + public static String getCurrentUsername() { + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + throw new BadRequestException(HttpStatus.UNAUTHORIZED, "当前登录状态过期"); + } + if (authentication.getPrincipal() instanceof UserDetails) { + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + return userDetails.getUsername(); + } + throw new BadRequestException(HttpStatus.UNAUTHORIZED, "找不到当前登录的信息"); + } + + /** + * 获取系统用户ID + * @return 系统用户ID + */ + public static Long getCurrentUserId() { + UserDetails userDetails = getCurrentUser(); + return new JSONObject(new JSONObject(userDetails).get("user")).get("id", Long.class); + } + + /** + * 获取当前用户的数据权限 + * @return / + */ + public static List getCurrentUserDataScope(){ + UserDetails userDetails = getCurrentUser(); + JSONArray array = JSONUtil.parseArray(new JSONObject(userDetails).get("dataScopes")); + return JSONUtil.toList(array,Long.class); + } + +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/utils/SpringContextHolder.java b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/SpringContextHolder.java new file mode 100644 index 0000000..83ae8c1 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/SpringContextHolder.java @@ -0,0 +1,145 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.utils; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.env.Environment; + +import java.util.ArrayList; +import java.util.List; +/** + * @author Jie + * @date 2019-01-07 + */ +@Slf4j +public class SpringContextHolder implements ApplicationContextAware, DisposableBean { + + private static ApplicationContext applicationContext = null; + private static final List CALL_BACKS = new ArrayList<>(); + private static boolean addCallback = true; + + /** + * 针对 某些初始化方法,在SpringContextHolder 未初始化时 提交回调方法。 + * 在SpringContextHolder 初始化后,进行回调使用 + * + * @param callBack 回调函数 + */ + public synchronized static void addCallBacks(CallBack callBack) { + if (addCallback) { + SpringContextHolder.CALL_BACKS.add(callBack); + } else { + log.warn("CallBack:{} 已无法添加!立即执行", callBack.getCallBackName()); + callBack.executor(); + } + } + + /** + * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型. + */ + @SuppressWarnings("unchecked") + public static T getBean(String name) { + assertContextInjected(); + return (T) applicationContext.getBean(name); + } + + /** + * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型. + */ + public static T getBean(Class requiredType) { + assertContextInjected(); + return applicationContext.getBean(requiredType); + } + + /** + * 获取SpringBoot 配置信息 + * + * @param property 属性key + * @param defaultValue 默认值 + * @param requiredType 返回类型 + * @return / + */ + public static T getProperties(String property, T defaultValue, Class requiredType) { + T result = defaultValue; + try { + result = getBean(Environment.class).getProperty(property, requiredType); + } catch (Exception ignored) {} + return result; + } + + /** + * 获取SpringBoot 配置信息 + * + * @param property 属性key + * @return / + */ + public static String getProperties(String property) { + return getProperties(property, null, String.class); + } + + /** + * 获取SpringBoot 配置信息 + * + * @param property 属性key + * @param requiredType 返回类型 + * @return / + */ + public static T getProperties(String property, Class requiredType) { + return getProperties(property, null, requiredType); + } + + /** + * 检查ApplicationContext不为空. + */ + private static void assertContextInjected() { + if (applicationContext == null) { + throw new IllegalStateException("applicaitonContext属性未注入, 请在applicationContext" + + ".xml中定义SpringContextHolder或在SpringBoot启动类中注册SpringContextHolder."); + } + } + + /** + * 清除SpringContextHolder中的ApplicationContext为Null. + */ + private static void clearHolder() { + log.debug("清除SpringContextHolder中的ApplicationContext:" + + applicationContext); + applicationContext = null; + } + + @Override + public void destroy() { + SpringContextHolder.clearHolder(); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + if (SpringContextHolder.applicationContext != null) { + log.warn("SpringContextHolder中的ApplicationContext被覆盖, 原有ApplicationContext为:" + SpringContextHolder.applicationContext); + } + SpringContextHolder.applicationContext = applicationContext; + if (addCallback) { + for (CallBack callBack : SpringContextHolder.CALL_BACKS) { + callBack.executor(); + } + CALL_BACKS.clear(); + } + SpringContextHolder.addCallback = false; + } +} diff --git a/backend/platform-system/src/main/java/com/yfd/platform/system/utils/StringUtils.java b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/StringUtils.java new file mode 100644 index 0000000..d791727 --- /dev/null +++ b/backend/platform-system/src/main/java/com/yfd/platform/system/utils/StringUtils.java @@ -0,0 +1,305 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yfd.platform.system.utils; + + +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.yfd.platform.constant.Constant; +import lombok.SneakyThrows; +import org.lionsoul.ip2region.DataBlock; +import org.lionsoul.ip2region.DbConfig; +import org.lionsoul.ip2region.DbSearcher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.ClassPathResource; +import eu.bitwalker.useragentutils.Browser; +import eu.bitwalker.useragentutils.UserAgent; +import jakarta.servlet.http.HttpServletRequest; +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.UnknownHostException; +import java.util.Calendar; +import java.util.Date; +import java.util.Enumeration; + +/** + * @author + * 字符串工具类, 继承org.apache.commons.lang3.StringUtils类 + */ +public class StringUtils extends org.apache.commons.lang3.StringUtils { + + private static final Logger log = LoggerFactory.getLogger(StringUtils.class); + private static boolean ipLocal = false; + private static File file ; + private static DbConfig config; + private static final char SEPARATOR = '_'; + private static final String UNKNOWN = "unknown"; + + static { + SpringContextHolder.addCallBacks(() -> { + StringUtils.ipLocal = SpringContextHolder.getProperties("ip.local-parsing", false, Boolean.class); + if (ipLocal) { + /* + * 此文件为独享 ,不必关闭 + */ + String path = "ip2region/ip2region.db"; + String name = "ip2region.db"; + try { + config = new DbConfig(); + file = FileUtil.inputStreamToFile(new ClassPathResource(path).getInputStream(), name); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + }); + } + + /** + * 驼峰命名法工具 + * + * @return toCamelCase(" hello_world ") == "helloWorld" + * toCapitalizeCamelCase("hello_world") == "HelloWorld" + * toUnderScoreCase("helloWorld") = "hello_world" + */ + public static String toCamelCase(String s) { + if (s == null) { + return null; + } + + s = s.toLowerCase(); + + StringBuilder sb = new StringBuilder(s.length()); + boolean upperCase = false; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + + if (c == SEPARATOR) { + upperCase = true; + } else if (upperCase) { + sb.append(Character.toUpperCase(c)); + upperCase = false; + } else { + sb.append(c); + } + } + + return sb.toString(); + } + + /** + * 驼峰命名法工具 + * + * @return toCamelCase(" hello_world ") == "helloWorld" + * toCapitalizeCamelCase("hello_world") == "HelloWorld" + * toUnderScoreCase("helloWorld") = "hello_world" + */ + public static String toCapitalizeCamelCase(String s) { + if (s == null) { + return null; + } + s = toCamelCase(s); + return s.substring(0, 1).toUpperCase() + s.substring(1); + } + + /** + * 驼峰命名法工具 + * + * @return toCamelCase(" hello_world ") == "helloWorld" + * toCapitalizeCamelCase("hello_world") == "HelloWorld" + * toUnderScoreCase("helloWorld") = "hello_world" + */ + static String toUnderScoreCase(String s) { + if (s == null) { + return null; + } + + StringBuilder sb = new StringBuilder(); + boolean upperCase = false; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + + boolean nextUpperCase = true; + + if (i < (s.length() - 1)) { + nextUpperCase = Character.isUpperCase(s.charAt(i + 1)); + } + + if ((i > 0) && Character.isUpperCase(c)) { + if (!upperCase || !nextUpperCase) { + sb.append(SEPARATOR); + } + upperCase = true; + } else { + upperCase = false; + } + + sb.append(Character.toLowerCase(c)); + } + + return sb.toString(); + } + + /** + * 获取ip地址 + */ + public static String getIp(HttpServletRequest request) { + String ip = request.getHeader("x-forwarded-for"); + if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + String comma = ","; + String localhost = "127.0.0.1"; + if (ip.contains(comma)) { + ip = ip.split(",")[0]; + } + if (localhost.equals(ip)) { + // 获取本机真正的ip地址 + try { + ip = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error(e.getMessage(), e); + } + } + return ip; + } + + /** + * 根据ip获取详细地址 + */ + @SneakyThrows + public static String getCityInfo(String ip) { + if (ipLocal) { + return getLocalCityInfo(ip); + } else { + return getHttpCityInfo(ip); + } + } + + /** + * 根据ip获取详细地址 + */ + public static String getHttpCityInfo(String ip) { + String host = "202.108.22.5"; + //超时应该在3钞以上 + int timeOut = 3000; + boolean status = false; + try { + status = InetAddress.getByName(host).isReachable(timeOut); + } catch (IOException e) { + e.printStackTrace(); + } + String api =""; + if (status){ + api = HttpUtil.get(String.format(Constant.Url.IP_URL, ip)); + }else { + api = "{\"ip\":\"127.0.0.1\",\"pro\":\"\",\"proCode\":\"999999\",\"city\":\"\",\"cityCode\":\"0\",\"region\":\"\",\"regionCode\":\"0\",\"addr\":\" 局域网\",\"regionNames\":\"\",\"err\":\"noprovince\"}"; + } + JSONObject object = JSONUtil.parseObj(api); + return object.get("addr", String.class); + } + + + /** + * 根据ip获取详细地址 + */ + public static String getLocalCityInfo(String ip) { + try { + DataBlock dataBlock = new DbSearcher(config, file.getPath()) + .binarySearch(ip); + String region = dataBlock.getRegion(); + String address = region.replace("0|", ""); + char symbol = '|'; + if (address.charAt(address.length() - 1) == symbol) { + address = address.substring(0, address.length() - 1); + } + return address.equals(Constant.REGION) ? "内网IP" : address; + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return ""; + } + + public static String getBrowser(HttpServletRequest request) { + UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent")); + Browser browser = userAgent.getBrowser(); + return browser.getName(); + } + + /** + * 获得当天是周几 + */ + public static String getWeekDay() { + String[] weekDays = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; + Calendar cal = Calendar.getInstance(); + cal.setTime(new Date()); + + int w = cal.get(Calendar.DAY_OF_WEEK) - 1; + if (w < 0) { + w = 0; + } + return weekDays[w]; + } + + /** + * 获取当前机器的IP + * + * @return / + */ + public static String getLocalIp() { + try { + InetAddress candidateAddress = null; + // 遍历所有的网络接口 + for (Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); interfaces.hasMoreElements();) { + NetworkInterface anInterface = interfaces.nextElement(); + // 在所有的接口下再遍历IP + for (Enumeration inetAddresses = anInterface.getInetAddresses(); inetAddresses.hasMoreElements();) { + InetAddress inetAddr = inetAddresses.nextElement(); + // 排除loopback类型地址 + if (!inetAddr.isLoopbackAddress()) { + if (inetAddr.isSiteLocalAddress()) { + // 如果是site-local地址,就是它了 + return inetAddr.getHostAddress(); + } else if (candidateAddress == null) { + // site-local类型的地址未被发现,先记录候选地址 + candidateAddress = inetAddr; + } + } + } + } + if (candidateAddress != null) { + return candidateAddress.getHostAddress(); + } + // 如果没有发现 non-loopback地址.只能用最次选的方案 + InetAddress jdkSuppliedAddress = InetAddress.getLocalHost(); + if (jdkSuppliedAddress == null) { + return ""; + } + return jdkSuppliedAddress.getHostAddress(); + } catch (Exception e) { + return ""; + } + } +} diff --git a/backend/platform-system/src/main/resources/application-dev.yml b/backend/platform-system/src/main/resources/application-dev.yml new file mode 100644 index 0000000..37c1cdd --- /dev/null +++ b/backend/platform-system/src/main/resources/application-dev.yml @@ -0,0 +1,94 @@ +server: + port: 8093 + +spring: + #应用名称 + application: + name: Project-plateform + datasource: + type: com.alibaba.druid.pool.DruidDataSource + druid: + master: + driverClassName: dm.jdbc.driver.DmDriver + url: "${DB_MASTER_URL:jdbc:dm://localhost:5236/WPPDB}" + username: "${DB_MASTER_USERNAME:WPPDB}" + password: "${DB_MASTER_PASSWORD:}" + slave: + driverClassName: dm.jdbc.driver.DmDriver + url: "${DB_SLAVE_URL:jdbc:dm://localhost:5236/WPPDB}" + username: "${DB_SLAVE_USERNAME:WPPDB}" + password: "${DB_SLAVE_PASSWORD:}" + + mvc: + pathmatch: + matching-strategy: ant_path_matcher + servlet: + multipart: + max-file-size: 30MB + max-request-size: 100MB + +logging: + file: + name: logs/projectname.log + level: + com.genersoft.iot: debug + com.genersoft.iot.vmp.storager.dao: info + com.genersoft.iot.vmp.gb28181: info + +# 在线文档: swagger-ui(生产环境建议关闭) +swagger-ui: + enabled: true + + +# 登录相关配置 +login: + # 登录缓存 + cache-enable: true + # 是否限制单用户登录 + single-login: false + # 验证码 + login-code: + # 验证码类型配置 查看 LoginProperties 类 + code-type: arithmetic + +# 启动自动数据库初始化(仅 dev/server): +app: + init: + enabled: false + schema: classpath:db-init/sql/min-schema.sql + # data 文件可选;为避免复杂 dump 解析问题,先不导入 + # data: + marker-table: sys_user + marker-version: v1.0.0 + # 登录图形验证码有效时间/分钟 + expiration: 2 + # 验证码高度 + width: 111 + # 验证码宽度 + heigth: 36 + # 内容长度 + length: 2 + # 字体名称,为空则使用默认字体 + font-name: + # 字体大小 + font-size: 25 + +# IP 本地解析 +ip: + local-parsing: true + + +file-space: #项目文档空间 + files: D:\demoproject\files\ #单独上传的文件附件 + system: D:\demoproject\system\ #单独上传的文件 + +task: + pool: + # 核心线程池大小 + core-pool-size: 10 + # 最大线程数 + max-pool-size: 30 + # 活跃时间 + keep-alive-seconds: 60 + # 队列容量 + queue-capacity: 50 diff --git a/backend/platform-system/src/main/resources/application-devtw.yml b/backend/platform-system/src/main/resources/application-devtw.yml new file mode 100644 index 0000000..f59a149 --- /dev/null +++ b/backend/platform-system/src/main/resources/application-devtw.yml @@ -0,0 +1,94 @@ +server: + port: 8093 + +spring: + #应用名称 + application: + name: Project-plateform + datasource: + type: com.alibaba.druid.pool.DruidDataSource + druid: + master: + driverClassName: oracle.jdbc.OracleDriver + url: "${DB_MASTER_URL:jdbc:oracle:thin:@172.16.21.134:1521/SDLYZ}" + username: "${DB_MASTER_USERNAME:QGC}" + password: "${DB_MASTER_PASSWORD:ar6Yr7Vxo5}" + slave: + driverClassName: oracle.jdbc.OracleDriver + url: "${DB_SLAVE_URL:jdbc:oracle:thin:@172.16.21.134:1521/SDLYZ}" + username: "${DB_SLAVE_USERNAME:QGC}" + password: "${DB_SLAVE_PASSWORD:ar6Yr7Vxo5}" + + mvc: + pathmatch: + matching-strategy: ant_path_matcher + servlet: + multipart: + max-file-size: 30MB + max-request-size: 100MB + +logging: + file: + name: logs/projectname.log + level: + com.genersoft.iot: debug + com.genersoft.iot.vmp.storager.dao: info + com.genersoft.iot.vmp.gb28181: info + +# 在线文档: swagger-ui(生产环境建议关闭) +swagger-ui: + enabled: true + + +# 登录相关配置 +login: + # 登录缓存 + cache-enable: true + # 是否限制单用户登录 + single-login: false + # 验证码 + login-code: + # 验证码类型配置 查看 LoginProperties 类 + code-type: arithmetic + +# 启动自动数据库初始化(仅 dev/server): +app: + init: + enabled: false + schema: classpath:db-init/sql/min-schema.sql + # data 文件可选;为避免复杂 dump 解析问题,先不导入 + # data: + marker-table: sys_user + marker-version: v1.0.0 + # 登录图形验证码有效时间/分钟 + expiration: 2 + # 验证码高度 + width: 111 + # 验证码宽度 + heigth: 36 + # 内容长度 + length: 2 + # 字体名称,为空则使用默认字体 + font-name: + # 字体大小 + font-size: 25 + +# IP 本地解析 +ip: + local-parsing: true + + +file-space: #项目文档空间 + files: D:\demoproject\files\ #单独上传的文件附件 + system: D:\demoproject\system\ #单独上传的文件 + +task: + pool: + # 核心线程池大小 + core-pool-size: 10 + # 最大线程数 + max-pool-size: 30 + # 活跃时间 + keep-alive-seconds: 60 + # 队列容量 + queue-capacity: 50 diff --git a/backend/platform-system/src/main/resources/application-framework.yml b/backend/platform-system/src/main/resources/application-framework.yml new file mode 100644 index 0000000..9baea68 --- /dev/null +++ b/backend/platform-system/src/main/resources/application-framework.yml @@ -0,0 +1,30 @@ +jasypt: + encryptor: + password: ${JASYPT_PASSWORD:} + +# 密码加密传输,前端公钥加密,后端私钥解密(共性配置) +rsa: + private_key: ${RSA_PRIVATE_KEY:} + +# Actuator & Micrometer 默认配置(共性) +management: + endpoints: + web: + exposure: + include: health,info,metrics,prometheus,env,beans,threaddump,loggers,configprops + endpoint: + health: + show-details: always + metrics: + tags: + application: ${spring.application.name:platform} + + +# Springdoc 默认配置(共性) +springdoc: + api-docs: + enabled: true + swagger-ui: + enabled: true + path: /swagger-ui.html + packages-to-scan: com.yfd.platform diff --git a/backend/platform-system/src/main/resources/application-server.yml b/backend/platform-system/src/main/resources/application-server.yml new file mode 100644 index 0000000..b4e0057 --- /dev/null +++ b/backend/platform-system/src/main/resources/application-server.yml @@ -0,0 +1,52 @@ +server: + port: 8090 + +spring: + #应用名称 + application: + name: Project-plateform + datasource: + type: com.alibaba.druid.pool.DruidDataSource + druid: + master: + driverClassName: com.mysql.cj.jdbc.Driver + url: "${DB_MASTER_URL:jdbc:mysql://43.138.168.68:3306/frameworkdb2025?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true}" + username: "${DB_MASTER_USERNAME:root}" + password: "${DB_MASTER_PASSWORD:}" + + mvc: + pathmatch: + matching-strategy: ant_path_matcher + servlet: + multipart: + max-file-size: 30MB + max-request-size: 100MB + +logging: + file: + name: logs/projectname.log + + level: + com.genersoft.iot: debug + com.genersoft.iot.vmp.storager.dao: info + com.genersoft.iot.vmp.gb28181: info + +# 在线文档: swagger-ui(生产环境建议关闭) +swagger-ui: + enabled: false + +file-space: #项目文档空间 + files: D:\demoproject\files\ #单独上传的文件附件 + useravatar: D:\demoproject\useravatar\ #用户头像 + system: D:\demoproject\system\ #系统文档根目录,用于头像等静态资源 + +# 启动自动数据库初始化(仅 dev/server): +app: + init: + enabled: true + schema: classpath:db-init/sql/min-schema.sql + # data 文件可选;为避免复杂 dump 解析问题,先不导入 + # data: + marker-table: sys_user + marker-version: v1.0.0 + diff --git a/backend/platform-system/src/main/resources/application.yml b/backend/platform-system/src/main/resources/application.yml new file mode 100644 index 0000000..8f42778 --- /dev/null +++ b/backend/platform-system/src/main/resources/application.yml @@ -0,0 +1,34 @@ +spring: + profiles: + active: devtw + +jasypt: + encryptor: + password: ${JASYPT_PASSWORD:} + +#密码加密传输,前端公钥加密,后端私钥解密 +rsa: + private_key: ${RSA_PRIVATE_KEY:MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEA0vfvyTdGJkdbHkB8mp0f3FE0GYP3AYPaJF7jUd1M0XxFSE2ceK3k2kw20YvQ09NJKk+OMjWQl9WitG9pB6tSCQIDAQABAkA2SimBrWC2/wvauBuYqjCFwLvYiRYqZKThUS3MZlebXJiLB+Ue/gUifAAKIg1avttUZsHBHrop4qfJCwAI0+YRAiEA+W3NK/RaXtnRqmoUUkb59zsZUBLpvZgQPfj1MhyHDz0CIQDYhsAhPJ3mgS64NbUZmGWuuNKp5coY2GIj/zYDMJp6vQIgUueLFXv/eZ1ekgz2Oi67MNCk5jeTF2BurZqNLR3MSmUCIFT3Q6uHMtsB9Eha4u7hS31tj1UWE+D+ADzp59MGnoftAiBeHT7gDMuqeJHPL4b+kC+gzV4FGTfhR9q3tTbklZkD2A==} + +# Actuator & Micrometer 默认配置 +management: + endpoints: + web: + exposure: + include: health,info,metrics,prometheus,env,beans,threaddump,loggers,configprops + endpoint: + health: + show-details: always + metrics: + tags: + application: ${spring.application.name:platform} + + +# Springdoc 默认配置 +springdoc: + api-docs: + enabled: true + swagger-ui: + enabled: true + path: /swagger-ui.html + packages-to-scan: com.yfd.platform diff --git a/backend/platform-system/src/main/resources/banner.txt b/backend/platform-system/src/main/resources/banner.txt new file mode 100644 index 0000000..47e51d1 --- /dev/null +++ b/backend/platform-system/src/main/resources/banner.txt @@ -0,0 +1,8 @@ + __ ____ ____ .___________. _______ ______ __ __ + | | \ \ / / | || ____| / || | | | + | | \ \/ / `---| |----`| |__ | ,----'| |__| | + .--. | | \_ _/ | | | __| | | | __ | + | `--' | | | | | | |____ | `----.| | | | + \______/ |__| |__| |_______| \______||__| |__| + + diff --git a/backend/platform-system/src/main/resources/ip2region/ip2region.db b/backend/platform-system/src/main/resources/ip2region/ip2region.db new file mode 100644 index 0000000..e69de29 diff --git a/backend/platform-system/src/main/resources/log4jdbc.log4j2.properties b/backend/platform-system/src/main/resources/log4jdbc.log4j2.properties new file mode 100644 index 0000000..302525f --- /dev/null +++ b/backend/platform-system/src/main/resources/log4jdbc.log4j2.properties @@ -0,0 +1,4 @@ +# If you use SLF4J. First, you need to tell log4jdbc-log4j2 that you want to use the SLF4J logger +log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator +log4jdbc.auto.load.popular.drivers=false +log4jdbc.drivers=com.mysql.cj.jdbc.Driver \ No newline at end of file diff --git a/backend/platform-system/src/main/resources/logback-spring.xml b/backend/platform-system/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..31a0ed8 --- /dev/null +++ b/backend/platform-system/src/main/resources/logback-spring.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + UTF-8 + ${LOG_PATTERN} + + + + + + ${LOG_PATH}/${LOG_FILE}.log + + UTF-8 + ${LOG_PATTERN} + + + ${LOG_PATH}/${LOG_FILE}.%d{yyyy-MM-dd}.log + 30 + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/platform-system/src/main/resources/logback.xml b/backend/platform-system/src/main/resources/logback.xml new file mode 100644 index 0000000..c949c89 --- /dev/null +++ b/backend/platform-system/src/main/resources/logback.xml @@ -0,0 +1,45 @@ + + + yfAdmin + + + + + + + ${log.pattern} + ${log.charset} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/platform-system/src/main/resources/mapper/system/DictionaryMapper.xml b/backend/platform-system/src/main/resources/mapper/system/DictionaryMapper.xml new file mode 100644 index 0000000..16edd1e --- /dev/null +++ b/backend/platform-system/src/main/resources/mapper/system/DictionaryMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/backend/platform-system/src/main/resources/mapper/system/MessageMapper.xml b/backend/platform-system/src/main/resources/mapper/system/MessageMapper.xml new file mode 100644 index 0000000..96cbcfc --- /dev/null +++ b/backend/platform-system/src/main/resources/mapper/system/MessageMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/backend/platform-system/src/main/resources/mapper/system/Model3dMapper.xml b/backend/platform-system/src/main/resources/mapper/system/Model3dMapper.xml new file mode 100644 index 0000000..d935f68 --- /dev/null +++ b/backend/platform-system/src/main/resources/mapper/system/Model3dMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/backend/platform-system/src/main/resources/mapper/system/QuartzJobMapper.xml b/backend/platform-system/src/main/resources/mapper/system/QuartzJobMapper.xml new file mode 100644 index 0000000..b523b0a --- /dev/null +++ b/backend/platform-system/src/main/resources/mapper/system/QuartzJobMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/backend/platform-system/src/main/resources/mapper/system/SysConfigMapper.xml b/backend/platform-system/src/main/resources/mapper/system/SysConfigMapper.xml new file mode 100644 index 0000000..cf22aa4 --- /dev/null +++ b/backend/platform-system/src/main/resources/mapper/system/SysConfigMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/backend/platform-system/src/main/resources/mapper/system/SysDictionaryItemsMapper.xml b/backend/platform-system/src/main/resources/mapper/system/SysDictionaryItemsMapper.xml new file mode 100644 index 0000000..1bf0942 --- /dev/null +++ b/backend/platform-system/src/main/resources/mapper/system/SysDictionaryItemsMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/backend/platform-system/src/main/resources/mapper/system/SysDictionaryMapper.xml b/backend/platform-system/src/main/resources/mapper/system/SysDictionaryMapper.xml new file mode 100644 index 0000000..6963e40 --- /dev/null +++ b/backend/platform-system/src/main/resources/mapper/system/SysDictionaryMapper.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/backend/platform-system/src/main/resources/mapper/system/SysLogMapper.xml b/backend/platform-system/src/main/resources/mapper/system/SysLogMapper.xml new file mode 100644 index 0000000..1046de9 --- /dev/null +++ b/backend/platform-system/src/main/resources/mapper/system/SysLogMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/backend/platform-system/src/main/resources/mapper/system/SysMenuMapper.xml b/backend/platform-system/src/main/resources/mapper/system/SysMenuMapper.xml new file mode 100644 index 0000000..85869c7 --- /dev/null +++ b/backend/platform-system/src/main/resources/mapper/system/SysMenuMapper.xml @@ -0,0 +1,103 @@ + + + + + + update sys_menu set orderno=orderno+1 where parentid=#{parentid} and orderno < #{Orderno} and orderno >= #{upOrderno} + + + + + update sys_menu SET orderno=orderno-1 where parentid=#{parentid} and orderno > #{Orderno} and orderno <= #{downOrderno} + + + + + + + + + + diff --git a/backend/platform-system/src/main/resources/mapper/system/SysMessageMapper.xml b/backend/platform-system/src/main/resources/mapper/system/SysMessageMapper.xml new file mode 100644 index 0000000..cda7c6f --- /dev/null +++ b/backend/platform-system/src/main/resources/mapper/system/SysMessageMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/backend/platform-system/src/main/resources/mapper/system/SysOrganizationMapper.xml b/backend/platform-system/src/main/resources/mapper/system/SysOrganizationMapper.xml new file mode 100644 index 0000000..053c5ae --- /dev/null +++ b/backend/platform-system/src/main/resources/mapper/system/SysOrganizationMapper.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/backend/platform-system/src/main/resources/mapper/system/SysQuartzJobMapper.xml b/backend/platform-system/src/main/resources/mapper/system/SysQuartzJobMapper.xml new file mode 100644 index 0000000..268cb3d --- /dev/null +++ b/backend/platform-system/src/main/resources/mapper/system/SysQuartzJobMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/backend/platform-system/src/main/resources/mapper/system/SysRoleMapper.xml b/backend/platform-system/src/main/resources/mapper/system/SysRoleMapper.xml new file mode 100644 index 0000000..c2c0d97 --- /dev/null +++ b/backend/platform-system/src/main/resources/mapper/system/SysRoleMapper.xml @@ -0,0 +1,150 @@ + + + + + INSERT INTO sys_role_menu + + + id, + + + roleid, + + + menuid + + + + + #{id}, + + + #{roleid}, + + + #{menuid} + + + + + + + + + + + + + + + + + + + + + + + + + delete from sys_role_users where userid !=(select u.id from sys_user u where u.account="admin") and roleid=#{roleid} and userid=#{urserid} + + + + + DELETE FROM sys_role_menu WHERE roleid= #{id} + + + + + DELETE FROM sys_role_users WHERE roleid= #{id} + + diff --git a/backend/platform-system/src/main/resources/mapper/system/SysUserMapper.xml b/backend/platform-system/src/main/resources/mapper/system/SysUserMapper.xml new file mode 100644 index 0000000..dfc56f0 --- /dev/null +++ b/backend/platform-system/src/main/resources/mapper/system/SysUserMapper.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + insert into sys_role_users values (#{id},#{roleid},#{userid}) + + + + + + + + + + + + + + + + + + delete from sys_role_users where userid=#{userid} + + + + delete from sys_role_users + where + userid=#{userid} + and roleid not in + + #{roleids} + + + + DELETE FROM sys_role_users WHERE userid IN + + #{id} + + + + diff --git a/backend/platform-system/src/main/resources/quartz.properties b/backend/platform-system/src/main/resources/quartz.properties new file mode 100644 index 0000000..03d3988 --- /dev/null +++ b/backend/platform-system/src/main/resources/quartz.properties @@ -0,0 +1,21 @@ +######################################## +# Quartz 默认配置示例(RAMJobStore) +######################################## + +org.quartz.scheduler.instanceName = PlatformScheduler +org.quartz.scheduler.instanceId = AUTO + +# 线程池配置 +org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool +org.quartz.threadPool.threadCount = 10 +org.quartz.threadPool.threadPriority = 5 + +# 使用内存存储(如需持久化请改为 JobStoreTX 并配置数据源) +org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore + +# Misfire 阈值 +org.quartz.jobStore.misfireThreshold = 60000 + +# 插件(可选):关闭时清理 +org.quartz.plugin.shutdownHook.class = org.quartz.plugins.management.ShutdownHookPlugin +org.quartz.plugin.shutdownHook.cleanShutdown = true \ No newline at end of file diff --git a/backend/platform-system/src/main/resources/static/assets/401-099a3a32.js b/backend/platform-system/src/main/resources/static/assets/401-099a3a32.js new file mode 100644 index 0000000..314a602 --- /dev/null +++ b/backend/platform-system/src/main/resources/static/assets/401-099a3a32.js @@ -0,0 +1 @@ +import{d as k,a9 as v,aa as x,c as V,g as o,w as s,a as i,Q as C,e as l,I as y,o as R,j as a,i as e,U as z,_ as B}from"./index-b25f0d08.js";const G={class:"errPage-container"},N={class:"list-unstyled"},U={class:"link-type"},j=["src"],I=["src"],P={name:"Page401"},h=k({...P,setup(E){const u=v({errGif:new URL("/assets/401-a61ddb94.gif",self.location).href,ewizardClap:"https://wpimg.wallstcn.com/007ef517-bafd-4066-aae4-6883632d9646",dialogVisible:!1}),{errGif:p,ewizardClap:_,dialogVisible:n}=x(u),f=y();function m(){f.back()}return(L,t)=>{const c=l("el-button"),b=l("router-link"),r=l("el-col"),g=l("el-row"),w=l("el-dialog");return R(),V("div",G,[o(c,{icon:"el-icon-arrow-left",class:"pan-back-btn",onClick:m},{default:s(()=>[...t[2]||(t[2]=[a(" 返回 ",-1)])]),_:1}),o(g,null,{default:s(()=>[o(r,{span:12},{default:s(()=>[t[6]||(t[6]=e("h1",{class:"text-jumbo text-ginormous"},"Oops!",-1)),t[7]||(t[7]=a(" gif来源",-1)),t[8]||(t[8]=e("a",{href:"https://zh.airbnb.com/",target:"_blank"},"airbnb",-1)),t[9]||(t[9]=a(" 页面 ",-1)),t[10]||(t[10]=e("h2",null,"你没有权限去该页面",-1)),t[11]||(t[11]=e("h6",null,"如有不满请联系你领导",-1)),e("ul",N,[t[4]||(t[4]=e("li",null,"或者你可以去:",-1)),e("li",U,[o(b,{to:"/dashboard"},{default:s(()=>[...t[3]||(t[3]=[a(" 回首页 ",-1)])]),_:1})]),t[5]||(t[5]=e("li",{class:"link-type"},[e("a",{href:"https://www.taobao.com/"},"随便看看")],-1)),e("li",null,[e("a",{href:"#",onClick:t[0]||(t[0]=z(d=>n.value=!0,["prevent"]))},"点我看图")])])]),_:1}),o(r,{span:12},{default:s(()=>[e("img",{src:i(p),width:"313",height:"428",alt:"Girl has dropped her ice cream."},null,8,j)]),_:1})]),_:1}),o(w,{modelValue:i(n),"onUpdate:modelValue":t[1]||(t[1]=d=>C(n)?n.value=d:null),title:"随便看"},{default:s(()=>[e("img",{src:i(_),class:"pan-img"},null,8,I)]),_:1},8,["modelValue"])])}}});const O=B(h,[["__scopeId","data-v-f88583c8"]]);export{O as default}; diff --git a/backend/platform-system/src/main/resources/static/assets/401-485a4475.js b/backend/platform-system/src/main/resources/static/assets/401-485a4475.js new file mode 100644 index 0000000..390a98a --- /dev/null +++ b/backend/platform-system/src/main/resources/static/assets/401-485a4475.js @@ -0,0 +1 @@ +import{d as v,aa as x,ab as V,c as C,f as t,w as o,a as i,S as y,i as a,M as I,o as R,j as _,h as e,W as S,J as z,K as B,_ as G}from"./index-5c62e6c4.js";const s=l=>(z("data-v-25007613"),l=l(),B(),l),N={class:"errPage-container"},j=s(()=>e("h1",{class:"text-jumbo text-ginormous"},"Oops!",-1)),M=s(()=>e("a",{href:"https://zh.airbnb.com/",target:"_blank"},"airbnb",-1)),P=s(()=>e("h2",null,"你没有权限去该页面",-1)),U=s(()=>e("h6",null,"如有不满请联系你领导",-1)),E={class:"list-unstyled"},J=s(()=>e("li",null,"或者你可以去:",-1)),K={class:"link-type"},L=s(()=>e("li",{class:"link-type"},[e("a",{href:"https://www.taobao.com/"},"随便看看")],-1)),O=["src"],T=["src"],W={name:"Page401"},$=v({...W,setup(l){const u=x({errGif:new URL("/assets/401-a61ddb94.gif",self.location).href,ewizardClap:"https://wpimg.wallstcn.com/007ef517-bafd-4066-aae4-6883632d9646",dialogVisible:!1}),{errGif:p,ewizardClap:f,dialogVisible:n}=V(u),h=I();function m(){h.back()}return(q,c)=>{const b=a("el-button"),g=a("router-link"),r=a("el-col"),w=a("el-row"),k=a("el-dialog");return R(),C("div",N,[t(b,{icon:"el-icon-arrow-left",class:"pan-back-btn",onClick:m},{default:o(()=>[_(" 返回 ")]),_:1}),t(w,null,{default:o(()=>[t(r,{span:12},{default:o(()=>[j,_(" gif来源"),M,_(" 页面 "),P,U,e("ul",E,[J,e("li",K,[t(g,{to:"/dashboard"},{default:o(()=>[_(" 回首页 ")]),_:1})]),L,e("li",null,[e("a",{href:"#",onClick:c[0]||(c[0]=S(d=>n.value=!0,["prevent"]))},"点我看图")])])]),_:1}),t(r,{span:12},{default:o(()=>[e("img",{src:i(p),width:"313",height:"428",alt:"Girl has dropped her ice cream."},null,8,O)]),_:1})]),_:1}),t(k,{modelValue:i(n),"onUpdate:modelValue":c[1]||(c[1]=d=>y(n)?n.value=d:null),title:"随便看"},{default:o(()=>[e("img",{src:i(f),class:"pan-img"},null,8,T)]),_:1},8,["modelValue"])])}}});const D=G($,[["__scopeId","data-v-25007613"]]);export{D as default}; diff --git a/backend/platform-system/src/main/resources/static/assets/401-a61ddb94.gif b/backend/platform-system/src/main/resources/static/assets/401-a61ddb94.gif new file mode 100644 index 0000000000000000000000000000000000000000..cd6e0d9433421b3f29d0ec0c40f755e354728000 GIT binary patch literal 164227 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⋘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 GY5fOuZZib{ literal 0 HcmV?d00001 diff --git a/backend/platform-system/src/main/resources/static/assets/401-fffd1e4b.css b/backend/platform-system/src/main/resources/static/assets/401-fffd1e4b.css new file mode 100644 index 0000000..6a09eaa --- /dev/null +++ b/backend/platform-system/src/main/resources/static/assets/401-fffd1e4b.css @@ -0,0 +1 @@ +.errPage-container[data-v-f88583c8]{width:800px;max-width:100%;margin:100px auto}.errPage-container .pan-back-btn[data-v-f88583c8]{background:#008489;color:#fff;border:none!important}.errPage-container .pan-gif[data-v-f88583c8]{margin:0 auto;display:block}.errPage-container .pan-img[data-v-f88583c8]{display:block;margin:0 auto;width:100%}.errPage-container .text-jumbo[data-v-f88583c8]{font-size:60px;font-weight:700;color:#484848}.errPage-container .list-unstyled[data-v-f88583c8]{font-size:14px}.errPage-container .list-unstyled li[data-v-f88583c8]{padding-bottom:5px}.errPage-container .list-unstyled a[data-v-f88583c8]{color:#008489;text-decoration:none}.errPage-container .list-unstyled a[data-v-f88583c8]:hover{text-decoration:underline} diff --git a/backend/platform-system/src/main/resources/static/assets/404-51ac6f86.css b/backend/platform-system/src/main/resources/static/assets/404-51ac6f86.css new file mode 100644 index 0000000..52dce10 --- /dev/null +++ b/backend/platform-system/src/main/resources/static/assets/404-51ac6f86.css @@ -0,0 +1 @@ +.wscn-http404-container[data-v-578ebab8]{transform:translate(-50%,-50%);position:absolute;top:40%;left:50%}.wscn-http404[data-v-578ebab8]{position:relative;width:1200px;padding:0 50px;overflow:hidden}.wscn-http404 .pic-404[data-v-578ebab8]{position:relative;float:left;width:600px;overflow:hidden}.wscn-http404 .pic-404__parent[data-v-578ebab8]{width:100%}.wscn-http404 .pic-404__child[data-v-578ebab8]{position:absolute}.wscn-http404 .pic-404__child.left[data-v-578ebab8]{width:80px;top:17px;left:220px;opacity:0;animation-name:cloudLeft-578ebab8;animation-duration:2s;animation-timing-function:linear;animation-fill-mode:forwards;animation-delay:1s}.wscn-http404 .pic-404__child.mid[data-v-578ebab8]{width:46px;top:10px;left:420px;opacity:0;animation-name:cloudMid-578ebab8;animation-duration:2s;animation-timing-function:linear;animation-fill-mode:forwards;animation-delay:1.2s}.wscn-http404 .pic-404__child.right[data-v-578ebab8]{width:62px;top:100px;left:500px;opacity:0;animation-name:cloudRight-578ebab8;animation-duration:2s;animation-timing-function:linear;animation-fill-mode:forwards;animation-delay:1s}@keyframes cloudLeft-578ebab8{0%{top:17px;left:220px;opacity:0}20%{top:33px;left:188px;opacity:1}80%{top:81px;left:92px;opacity:1}to{top:97px;left:60px;opacity:0}}@keyframes cloudMid-578ebab8{0%{top:10px;left:420px;opacity:0}20%{top:40px;left:360px;opacity:1}70%{top:130px;left:180px;opacity:1}to{top:160px;left:120px;opacity:0}}@keyframes cloudRight-578ebab8{0%{top:100px;left:500px;opacity:0}20%{top:120px;left:460px;opacity:1}80%{top:180px;left:340px;opacity:1}to{top:200px;left:300px;opacity:0}}.wscn-http404 .bullshit[data-v-578ebab8]{position:relative;float:left;width:300px;padding:30px 0;overflow:hidden}.wscn-http404 .bullshit__oops[data-v-578ebab8]{font-size:32px;font-weight:700;line-height:40px;color:#1482f0;opacity:0;margin-bottom:20px;animation-name:slideUp-578ebab8;animation-duration:.5s;animation-fill-mode:forwards}.wscn-http404 .bullshit__headline[data-v-578ebab8]{font-size:20px;line-height:24px;color:#222;font-weight:700;opacity:0;margin-bottom:10px;animation-name:slideUp-578ebab8;animation-duration:.5s;animation-delay:.1s;animation-fill-mode:forwards}.wscn-http404 .bullshit__info[data-v-578ebab8]{font-size:13px;line-height:21px;color:gray;opacity:0;margin-bottom:30px;animation-name:slideUp-578ebab8;animation-duration:.5s;animation-delay:.2s;animation-fill-mode:forwards}.wscn-http404 .bullshit__return-home[data-v-578ebab8]{display:block;float:left;width:110px;height:36px;background:#1482f0;border-radius:100px;text-align:center;color:#fff;opacity:0;font-size:14px;line-height:36px;cursor:pointer;animation-name:slideUp-578ebab8;animation-duration:.5s;animation-delay:.3s;animation-fill-mode:forwards}@keyframes slideUp-578ebab8{0%{transform:translateY(60px);opacity:0}to{transform:translateY(0);opacity:1}} diff --git a/backend/platform-system/src/main/resources/static/assets/404-538aa4d7.png b/backend/platform-system/src/main/resources/static/assets/404-538aa4d7.png new file mode 100644 index 0000000000000000000000000000000000000000..3d8e2305cc973ad2121403aee4bf08728f76c461 GIT binary patch literal 98071 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 literal 0 HcmV?d00001 diff --git a/backend/platform-system/src/main/resources/static/assets/404-6dcbdda2.js b/backend/platform-system/src/main/resources/static/assets/404-6dcbdda2.js new file mode 100644 index 0000000..7deafbf --- /dev/null +++ b/backend/platform-system/src/main/resources/static/assets/404-6dcbdda2.js @@ -0,0 +1 @@ +import{d as l,c as i,i as s,af as o,j as c,t as r,o as n,_}from"./index-b25f0d08.js";const d="/assets/404-538aa4d7.png",e="/assets/404_cloud-98e7ac66.png",p={class:"wscn-http404-container"},b={name:"Page404"},u=l({...b,setup(h){function a(){return"The webmaster said that you can not enter this page..."}return(m,t)=>(n(),i("div",p,[s("div",{class:"wscn-http404"},[t[4]||(t[4]=o('
404404404404
',1)),s("div",{class:"bullshit"},[t[0]||(t[0]=s("div",{class:"bullshit__oops"},"OOPS!",-1)),t[1]||(t[1]=s("div",{class:"bullshit__info"},[c(" All rights reserved "),s("a",{style:{color:"#20a0ff"},href:"https://wallstreetcn.com",target:"_blank"},"wallstreetcn")],-1)),s("div",{class:"bullshit__headline"},r(a)),t[2]||(t[2]=s("div",{class:"bullshit__info"}," Please check that the URL you entered is correct, or click the button below to return to the homepage. ",-1)),t[3]||(t[3]=s("a",{href:"",class:"bullshit__return-home"},"Back to home",-1))])])]))}});const f=_(u,[["__scopeId","data-v-578ebab8"]]);export{f as default}; diff --git a/backend/platform-system/src/main/resources/static/assets/404-ae343fa7.js b/backend/platform-system/src/main/resources/static/assets/404-ae343fa7.js new file mode 100644 index 0000000..51a23cb --- /dev/null +++ b/backend/platform-system/src/main/resources/static/assets/404-ae343fa7.js @@ -0,0 +1 @@ +import{d as o,c as _,h as t,t as i,ag as l,J as n,K as d,j as r,o as h,_ as p}from"./index-5c62e6c4.js";const f="/assets/404-538aa4d7.png",a="/assets/404_cloud-98e7ac66.png",e=s=>(n("data-v-ec8f1f5a"),s=s(),d(),s),u={class:"wscn-http404-container"},m=l('
404404404404
',1),v=e(()=>t("div",{class:"bullshit__oops"},"OOPS!",-1)),g=e(()=>t("div",{class:"bullshit__info"},[r(" All rights reserved "),t("a",{style:{color:"#20a0ff"},href:"https://wallstreetcn.com",target:"_blank"},"wallstreetcn")],-1)),b=e(()=>t("div",{class:"bullshit__info"}," Please check that the URL you entered is correct, or click the button below to return to the homepage. ",-1)),w=e(()=>t("a",{href:"",class:"bullshit__return-home"},"Back to home",-1)),k={name:"Page404"},y=o({...k,setup(s){function c(){return"The webmaster said that you can not enter this page..."}return(S,x)=>(h(),_("div",u,[t("div",{class:"wscn-http404"},[m,t("div",{class:"bullshit"},[v,g,t("div",{class:"bullshit__headline"},i(c)),b,w])])]))}});const I=p(y,[["__scopeId","data-v-ec8f1f5a"]]);export{I as default}; diff --git a/backend/platform-system/src/main/resources/static/assets/404_cloud-98e7ac66.png b/backend/platform-system/src/main/resources/static/assets/404_cloud-98e7ac66.png new file mode 100644 index 0000000000000000000000000000000000000000..c6281d09013e0a2c5f8e699a0a6038d9480291e5 GIT binary patch literal 4766 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 literal 0 HcmV?d00001 diff --git a/backend/platform-system/src/main/resources/static/assets/BarChart-a4765ae3.js b/backend/platform-system/src/main/resources/static/assets/BarChart-a4765ae3.js new file mode 100644 index 0000000..95b45f0 --- /dev/null +++ b/backend/platform-system/src/main/resources/static/assets/BarChart-a4765ae3.js @@ -0,0 +1 @@ +import{r as c,i as f,L as a}from"./resize-9f0962b6.js";import{d as m,I as y,aq as u,ar as x,k as h,a4 as p,o as g,c as b,X as S,p as v}from"./index-5c62e6c4.js";const w=["id"],L=m({__name:"BarChart",props:{id:{type:String,default:"barChart"},className:{type:String,default:""},width:{type:String,default:"200px",required:!0},height:{type:String,default:"200px",required:!0}},setup(e){const o=e,{mounted:i,chart:r,beforeDestroy:n,activated:l,deactivated:s}=c();function d(){const t=f(document.getElementById(o.id));t.setOption({title:{show:!0,text:"业绩总览",x:"center",padding:15,textStyle:{fontSize:18,fontStyle:"normal",fontWeight:"bold",color:"#337ecc"}},grid:{left:"2%",right:"2%",bottom:"10%",containLabel:!0},tooltip:{trigger:"axis",axisPointer:{type:"cross",crossStyle:{color:"#999"}}},legend:{x:"center",y:"bottom",data:["收入","毛利润","收入增长率","利润增长率"]},xAxis:[{type:"category",data:["浙江","北京","上海","广东","深圳"],axisPointer:{type:"shadow"}}],yAxis:[{type:"value",min:0,max:1e4,interval:2e3,axisLabel:{formatter:"{value} "}},{type:"value",min:0,max:100,interval:20,axisLabel:{formatter:"{value}%"}}],series:[{name:"收入",type:"bar",data:[7e3,7100,7200,7300,7400],barWidth:20,itemStyle:{color:new a(0,0,0,1,[{offset:0,color:"#83bff6"},{offset:.5,color:"#188df0"},{offset:1,color:"#188df0"}])}},{name:"毛利润",type:"bar",data:[8e3,8200,8400,8600,8800],barWidth:20,itemStyle:{color:new a(0,0,0,1,[{offset:0,color:"#25d73c"},{offset:.5,color:"#1bc23d"},{offset:1,color:"#179e61"}])}},{name:"收入增长率",type:"line",yAxisIndex:1,data:[60,65,70,75,80],itemStyle:{color:"#67C23A"}},{name:"利润增长率",type:"line",yAxisIndex:1,data:[70,75,80,85,90],itemStyle:{color:"#409EFF"}}]}),r.value=t}return y(()=>{n()}),u(()=>{l()}),x(()=>{s()}),h(()=>{i(),p(()=>{d()})}),(t,C)=>(g(),b("div",{id:e.id,class:S(e.className),style:v({height:e.height,width:e.width})},null,14,w))}});export{L as default}; diff --git a/backend/platform-system/src/main/resources/static/assets/BarChart-efd5cbe1.js b/backend/platform-system/src/main/resources/static/assets/BarChart-efd5cbe1.js new file mode 100644 index 0000000..87b1a1e --- /dev/null +++ b/backend/platform-system/src/main/resources/static/assets/BarChart-efd5cbe1.js @@ -0,0 +1 @@ +import{r as c,i as f,L as a}from"./resize-24879ea2.js";import{d as m,K as y,ap as u,aq as x,k as h,a3 as p,o as g,c as b,V as S,q as v}from"./index-b25f0d08.js";const w=["id"],L=m({__name:"BarChart",props:{id:{type:String,default:"barChart"},className:{type:String,default:""},width:{type:String,default:"200px",required:!0},height:{type:String,default:"200px",required:!0}},setup(e){const o=e,{mounted:i,chart:r,beforeDestroy:n,activated:l,deactivated:s}=c();function d(){const t=f(document.getElementById(o.id));t.setOption({title:{show:!0,text:"业绩总览",x:"center",padding:15,textStyle:{fontSize:18,fontStyle:"normal",fontWeight:"bold",color:"#337ecc"}},grid:{left:"2%",right:"2%",bottom:"10%",containLabel:!0},tooltip:{trigger:"axis",axisPointer:{type:"cross",crossStyle:{color:"#999"}}},legend:{x:"center",y:"bottom",data:["收入","毛利润","收入增长率","利润增长率"]},xAxis:[{type:"category",data:["浙江","北京","上海","广东","深圳"],axisPointer:{type:"shadow"}}],yAxis:[{type:"value",min:0,max:1e4,interval:2e3,axisLabel:{formatter:"{value} "}},{type:"value",min:0,max:100,interval:20,axisLabel:{formatter:"{value}%"}}],series:[{name:"收入",type:"bar",data:[7e3,7100,7200,7300,7400],barWidth:20,itemStyle:{color:new a(0,0,0,1,[{offset:0,color:"#83bff6"},{offset:.5,color:"#188df0"},{offset:1,color:"#188df0"}])}},{name:"毛利润",type:"bar",data:[8e3,8200,8400,8600,8800],barWidth:20,itemStyle:{color:new a(0,0,0,1,[{offset:0,color:"#25d73c"},{offset:.5,color:"#1bc23d"},{offset:1,color:"#179e61"}])}},{name:"收入增长率",type:"line",yAxisIndex:1,data:[60,65,70,75,80],itemStyle:{color:"#67C23A"}},{name:"利润增长率",type:"line",yAxisIndex:1,data:[70,75,80,85,90],itemStyle:{color:"#409EFF"}}]}),r.value=t}return y(()=>{n()}),u(()=>{l()}),x(()=>{s()}),h(()=>{i(),p(()=>{d()})}),(t,C)=>(g(),b("div",{id:e.id,class:S(e.className),style:v({height:e.height,width:e.width})},null,14,w))}});export{L as default}; diff --git a/backend/platform-system/src/main/resources/static/assets/FunnelChart-79f3d5f7.js b/backend/platform-system/src/main/resources/static/assets/FunnelChart-79f3d5f7.js new file mode 100644 index 0000000..0a2fd3d --- /dev/null +++ b/backend/platform-system/src/main/resources/static/assets/FunnelChart-79f3d5f7.js @@ -0,0 +1 @@ +import{r as s,i as u}from"./resize-24879ea2.js";import{d as c,K as m,ap as h,aq as f,k as p,a3 as y,o as g,c as S,V as b,q as v}from"./index-b25f0d08.js";const w=["id"],q=c({__name:"FunnelChart",props:{id:{type:String,default:"funnelChart"},className:{type:String,default:""},width:{type:String,default:"200px",required:!0},height:{type:String,default:"200px",required:!0}},setup(e){const n=e,{mounted:a,chart:i,beforeDestroy:o,activated:l,deactivated:r}=s();function d(){const t=u(document.getElementById(n.id));t.setOption({title:{show:!0,text:"订单线索转化漏斗图",x:"center",padding:15,textStyle:{fontSize:18,fontStyle:"normal",fontWeight:"bold",color:"#337ecc"}},grid:{left:"2%",right:"2%",bottom:"10%",containLabel:!0},legend:{x:"center",y:"bottom",data:["Show","Click","Visit","Inquiry","Order"]},series:[{name:"Funnel",type:"funnel",left:"20%",top:60,bottom:60,width:"60%",sort:"descending",gap:2,label:{show:!0,position:"inside"},labelLine:{length:10,lineStyle:{width:1,type:"solid"}},itemStyle:{borderColor:"#fff",borderWidth:1},emphasis:{label:{fontSize:20}},data:[{value:60,name:"Visit"},{value:40,name:"Inquiry"},{value:20,name:"Order"},{value:80,name:"Click"},{value:100,name:"Show"}]}]}),i.value=t}return m(()=>{o()}),h(()=>{l()}),f(()=>{r()}),p(()=>{a(),y(()=>{d()})}),(t,x)=>(g(),S("div",{id:e.id,class:b(e.className),style:v({height:e.height,width:e.width})},null,14,w))}});export{q as default}; diff --git a/backend/platform-system/src/main/resources/static/assets/FunnelChart-8e41d306.js b/backend/platform-system/src/main/resources/static/assets/FunnelChart-8e41d306.js new file mode 100644 index 0000000..b4f80e1 --- /dev/null +++ b/backend/platform-system/src/main/resources/static/assets/FunnelChart-8e41d306.js @@ -0,0 +1 @@ +import{r as s,i as u}from"./resize-9f0962b6.js";import{d as c,I as m,aq as h,ar as f,k as p,a4 as y,o as g,c as S,X as b,p as v}from"./index-5c62e6c4.js";const w=["id"],q=c({__name:"FunnelChart",props:{id:{type:String,default:"funnelChart"},className:{type:String,default:""},width:{type:String,default:"200px",required:!0},height:{type:String,default:"200px",required:!0}},setup(e){const n=e,{mounted:a,chart:i,beforeDestroy:o,activated:l,deactivated:r}=s();function d(){const t=u(document.getElementById(n.id));t.setOption({title:{show:!0,text:"订单线索转化漏斗图",x:"center",padding:15,textStyle:{fontSize:18,fontStyle:"normal",fontWeight:"bold",color:"#337ecc"}},grid:{left:"2%",right:"2%",bottom:"10%",containLabel:!0},legend:{x:"center",y:"bottom",data:["Show","Click","Visit","Inquiry","Order"]},series:[{name:"Funnel",type:"funnel",left:"20%",top:60,bottom:60,width:"60%",sort:"descending",gap:2,label:{show:!0,position:"inside"},labelLine:{length:10,lineStyle:{width:1,type:"solid"}},itemStyle:{borderColor:"#fff",borderWidth:1},emphasis:{label:{fontSize:20}},data:[{value:60,name:"Visit"},{value:40,name:"Inquiry"},{value:20,name:"Order"},{value:80,name:"Click"},{value:100,name:"Show"}]}]}),i.value=t}return m(()=>{o()}),h(()=>{l()}),f(()=>{r()}),p(()=>{a(),y(()=>{d()})}),(t,x)=>(g(),S("div",{id:e.id,class:b(e.className),style:v({height:e.height,width:e.width})},null,14,w))}});export{q as default}; diff --git a/backend/platform-system/src/main/resources/static/assets/PieChart-bffd7bcc.js b/backend/platform-system/src/main/resources/static/assets/PieChart-bffd7bcc.js new file mode 100644 index 0000000..06edc6a --- /dev/null +++ b/backend/platform-system/src/main/resources/static/assets/PieChart-bffd7bcc.js @@ -0,0 +1 @@ +import{r as c,i as u}from"./resize-24879ea2.js";import{d as m,K as h,ap as f,aq as p,k as g,a3 as y,o as C,c as v,V as x,q as S}from"./index-b25f0d08.js";const b=["id"],z=m({__name:"PieChart",props:{id:{type:String,default:"pieChart"},className:{type:String,default:""},width:{type:String,default:"200px",required:!0},height:{type:String,default:"200px",required:!0}},setup(e){const i=e,{mounted:n,chart:o,beforeDestroy:r,activated:s,deactivated:d}=c();function l(){const t=u(document.getElementById(i.id));t.setOption({title:{show:!0,text:"产品分类总览",x:"center",padding:15,textStyle:{fontSize:18,fontStyle:"normal",fontWeight:"bold",color:"#337ecc"}},grid:{left:"2%",right:"2%",bottom:"10%",containLabel:!0},legend:{top:"bottom"},series:[{name:"Nightingale Chart",type:"pie",radius:[50,130],center:["50%","50%"],roseType:"area",itemStyle:{borderRadius:1,color:function(a){return["#409EFF","#67C23A","#E6A23C","#F56C6C"][a.dataIndex]}},data:[{value:26,name:"家用电器"},{value:27,name:"户外运动"},{value:24,name:"汽车用品"},{value:23,name:"手机数码"}]}]}),o.value=t}return h(()=>{r()}),f(()=>{s()}),p(()=>{d()}),g(()=>{n(),y(()=>{l()})}),(t,a)=>(C(),v("div",{id:e.id,class:x(e.className),style:S({height:e.height,width:e.width})},null,14,b))}});export{z as default}; diff --git a/backend/platform-system/src/main/resources/static/assets/PieChart-f0d9d351.js b/backend/platform-system/src/main/resources/static/assets/PieChart-f0d9d351.js new file mode 100644 index 0000000..8f6ea0a --- /dev/null +++ b/backend/platform-system/src/main/resources/static/assets/PieChart-f0d9d351.js @@ -0,0 +1 @@ +import{r as c,i as u}from"./resize-9f0962b6.js";import{d as m,I as h,aq as f,ar as p,k as g,a4 as y,o as C,c as v,X as x,p as S}from"./index-5c62e6c4.js";const b=["id"],B=m({__name:"PieChart",props:{id:{type:String,default:"pieChart"},className:{type:String,default:""},width:{type:String,default:"200px",required:!0},height:{type:String,default:"200px",required:!0}},setup(e){const i=e,{mounted:n,chart:o,beforeDestroy:r,activated:s,deactivated:d}=c();function l(){const t=u(document.getElementById(i.id));t.setOption({title:{show:!0,text:"产品分类总览",x:"center",padding:15,textStyle:{fontSize:18,fontStyle:"normal",fontWeight:"bold",color:"#337ecc"}},grid:{left:"2%",right:"2%",bottom:"10%",containLabel:!0},legend:{top:"bottom"},series:[{name:"Nightingale Chart",type:"pie",radius:[50,130],center:["50%","50%"],roseType:"area",itemStyle:{borderRadius:1,color:function(a){return["#409EFF","#67C23A","#E6A23C","#F56C6C"][a.dataIndex]}},data:[{value:26,name:"家用电器"},{value:27,name:"户外运动"},{value:24,name:"汽车用品"},{value:23,name:"手机数码"}]}]}),o.value=t}return h(()=>{r()}),f(()=>{s()}),p(()=>{d()}),g(()=>{n(),y(()=>{l()})}),(t,a)=>(C(),v("div",{id:e.id,class:x(e.className),style:S({height:e.height,width:e.width})},null,14,b))}});export{B as default}; diff --git a/backend/platform-system/src/main/resources/static/assets/RadarChart-94b1112a.js b/backend/platform-system/src/main/resources/static/assets/RadarChart-94b1112a.js new file mode 100644 index 0000000..3a44423 --- /dev/null +++ b/backend/platform-system/src/main/resources/static/assets/RadarChart-94b1112a.js @@ -0,0 +1 @@ +import{r as l,i as m}from"./resize-9f0962b6.js";import{d as u,I as h,aq as f,ar as g,k as p,a4 as y,o as v,c as x,X as C,p as S}from"./index-5c62e6c4.js";const b=["id"],z=u({__name:"RadarChart",props:{id:{type:String,default:"radarChart"},className:{type:String,default:""},width:{type:String,default:"200px",required:!0},height:{type:String,default:"200px",required:!0}},setup(e){const n=e,{mounted:r,chart:i,beforeDestroy:o,activated:d,deactivated:s}=l();function c(){const t=m(document.getElementById(n.id));t.setOption({title:{show:!0,text:"订单状态统计",x:"center",padding:15,textStyle:{fontSize:18,fontStyle:"normal",fontWeight:"bold",color:"#337ecc"}},grid:{left:"2%",right:"2%",bottom:"10%",containLabel:!0},legend:{x:"center",y:"bottom",data:["预定数量","下单数量","发货数量"]},radar:{radius:"60%",indicator:[{name:"家用电器"},{name:"服装箱包"},{name:"运动户外"},{name:"手机数码"},{name:"汽车用品"},{name:"家具厨具"}]},series:[{name:"Budget vs spending",type:"radar",itemStyle:{borderRadius:6,color:function(a){return["#409EFF","#67C23A","#E6A23C","#F56C6C"][a.dataIndex]}},data:[{value:[400,400,400,400,400,400],name:"预定数量"},{value:[300,300,300,300,300,300],name:"下单数量"},{value:[200,200,200,200,200,200],name:"发货数量"}]}]}),i.value=t}return h(()=>{o()}),f(()=>{d()}),g(()=>{s()}),p(()=>{r(),y(()=>{c()})}),(t,a)=>(v(),x("div",{id:e.id,class:C(e.className),style:S({height:e.height,width:e.width})},null,14,b))}});export{z as default}; diff --git a/backend/platform-system/src/main/resources/static/assets/RadarChart-e43ec971.js b/backend/platform-system/src/main/resources/static/assets/RadarChart-e43ec971.js new file mode 100644 index 0000000..5f5722d --- /dev/null +++ b/backend/platform-system/src/main/resources/static/assets/RadarChart-e43ec971.js @@ -0,0 +1 @@ +import{r as l,i as m}from"./resize-24879ea2.js";import{d as u,K as h,ap as f,aq as g,k as p,a3 as y,o as v,c as x,V as C,q as S}from"./index-b25f0d08.js";const b=["id"],w=u({__name:"RadarChart",props:{id:{type:String,default:"radarChart"},className:{type:String,default:""},width:{type:String,default:"200px",required:!0},height:{type:String,default:"200px",required:!0}},setup(e){const n=e,{mounted:r,chart:i,beforeDestroy:o,activated:d,deactivated:s}=l();function c(){const t=m(document.getElementById(n.id));t.setOption({title:{show:!0,text:"订单状态统计",x:"center",padding:15,textStyle:{fontSize:18,fontStyle:"normal",fontWeight:"bold",color:"#337ecc"}},grid:{left:"2%",right:"2%",bottom:"10%",containLabel:!0},legend:{x:"center",y:"bottom",data:["预定数量","下单数量","发货数量"]},radar:{radius:"60%",indicator:[{name:"家用电器"},{name:"服装箱包"},{name:"运动户外"},{name:"手机数码"},{name:"汽车用品"},{name:"家具厨具"}]},series:[{name:"Budget vs spending",type:"radar",itemStyle:{borderRadius:6,color:function(a){return["#409EFF","#67C23A","#E6A23C","#F56C6C"][a.dataIndex]}},data:[{value:[400,400,400,400,400,400],name:"预定数量"},{value:[300,300,300,300,300,300],name:"下单数量"},{value:[200,200,200,200,200,200],name:"发货数量"}]}]}),i.value=t}return h(()=>{o()}),f(()=>{d()}),g(()=>{s()}),p(()=>{r(),y(()=>{c()})}),(t,a)=>(v(),x("div",{id:e.id,class:C(e.className),style:S({height:e.height,width:e.width})},null,14,b))}});export{w as default}; diff --git a/backend/platform-system/src/main/resources/static/assets/editor-501cf061.css b/backend/platform-system/src/main/resources/static/assets/editor-501cf061.css new file mode 100644 index 0000000..50ac020 --- /dev/null +++ b/backend/platform-system/src/main/resources/static/assets/editor-501cf061.css @@ -0,0 +1 @@ +:root,:host{--w-e-textarea-bg-color: #fff;--w-e-textarea-color: #333;--w-e-textarea-border-color: #ccc;--w-e-textarea-slight-border-color: #e8e8e8;--w-e-textarea-slight-color: #d4d4d4;--w-e-textarea-slight-bg-color: #f5f2f0;--w-e-textarea-selected-border-color: #B4D5FF;--w-e-textarea-handler-bg-color: #4290f7;--w-e-toolbar-color: #595959;--w-e-toolbar-bg-color: #fff;--w-e-toolbar-active-color: #333;--w-e-toolbar-active-bg-color: #f1f1f1;--w-e-toolbar-disabled-color: #999;--w-e-toolbar-border-color: #e8e8e8;--w-e-modal-button-bg-color: #fafafa;--w-e-modal-button-border-color: #d9d9d9}.w-e-text-container *,.w-e-toolbar *{box-sizing:border-box;margin:0;outline:none;padding:0}.w-e-text-container blockquote,.w-e-text-container li,.w-e-text-container p,.w-e-text-container td,.w-e-text-container th,.w-e-toolbar *{line-height:1.5}.w-e-text-container{background-color:var(--w-e-textarea-bg-color);color:var(--w-e-textarea-color);height:100%;position:relative}.w-e-text-container .w-e-scroll{-webkit-overflow-scrolling:touch;height:100%}.w-e-text-container [data-slate-editor]{word-wrap:break-word;border-top:1px solid transparent;min-height:100%;outline:0;padding:0 10px;white-space:pre-wrap}.w-e-text-container [data-slate-editor] p{margin:15px 0}.w-e-text-container [data-slate-editor] h1,.w-e-text-container [data-slate-editor] h2,.w-e-text-container [data-slate-editor] h3,.w-e-text-container [data-slate-editor] h4,.w-e-text-container [data-slate-editor] h5{margin:20px 0}.w-e-text-container [data-slate-editor] img{cursor:default;display:inline!important;max-width:100%;min-height:20px;min-width:20px}.w-e-text-container [data-slate-editor] span{text-indent:0}.w-e-text-container [data-slate-editor] [data-selected=true]{box-shadow:0 0 0 2px var(--w-e-textarea-selected-border-color)}.w-e-text-placeholder{font-style:italic;left:10px;top:17px;width:90%}.w-e-max-length-info,.w-e-text-placeholder{color:var(--w-e-textarea-slight-color);pointer-events:none;position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none}.w-e-max-length-info{bottom:.5em;right:1em}.w-e-bar{background-color:var(--w-e-toolbar-bg-color);color:var(--w-e-toolbar-color);font-size:14px;padding:0 5px}.w-e-bar svg{fill:var(--w-e-toolbar-color);height:14px;width:14px}.w-e-bar-show{display:flex}.w-e-bar-hidden{display:none}.w-e-hover-bar{border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 5px #0000001f;position:absolute}.w-e-toolbar{flex-wrap:wrap;position:relative}.w-e-bar-divider{background-color:var(--w-e-toolbar-border-color);display:inline-flex;height:40px;margin:0 5px;width:1px}.w-e-bar-item{display:flex;height:40px;padding:4px;position:relative;text-align:center}.w-e-bar-item,.w-e-bar-item button{align-items:center;justify-content:center}.w-e-bar-item button{background:transparent;border:none;color:var(--w-e-toolbar-color);cursor:pointer;display:inline-flex;height:32px;overflow:hidden;padding:0 8px;white-space:nowrap}.w-e-bar-item button:hover{background-color:var(--w-e-toolbar-active-bg-color);color:var(--w-e-toolbar-active-color)}.w-e-bar-item button .title{margin-left:5px}.w-e-bar-item .active{background-color:var(--w-e-toolbar-active-bg-color);color:var(--w-e-toolbar-active-color)}.w-e-bar-item .disabled{color:var(--w-e-toolbar-disabled-color);cursor:not-allowed}.w-e-bar-item .disabled svg{fill:var(--w-e-toolbar-disabled-color)}.w-e-bar-item .disabled:hover{background-color:var(--w-e-toolbar-bg-color);color:var(--w-e-toolbar-disabled-color)}.w-e-bar-item .disabled:hover svg{fill:var(--w-e-toolbar-disabled-color)}.w-e-menu-tooltip-v5:before{background-color:var(--w-e-toolbar-active-color);border-radius:5px;color:var(--w-e-toolbar-bg-color);content:attr(data-tooltip);font-size:.75em;opacity:0;padding:5px 10px;position:absolute;text-align:center;top:40px;transition:opacity .6s;visibility:hidden;white-space:pre;z-index:1}.w-e-menu-tooltip-v5:after{border:5px solid transparent;border-bottom:5px solid var(--w-e-toolbar-active-color);content:"";opacity:0;position:absolute;top:30px;transition:opacity .6s;visibility:hidden}.w-e-menu-tooltip-v5:hover:after,.w-e-menu-tooltip-v5:hover:before{opacity:1;visibility:visible}.w-e-menu-tooltip-v5.tooltip-right:before{left:100%;top:10px}.w-e-menu-tooltip-v5.tooltip-right:after{border-bottom-color:transparent;border-left-color:transparent;border-right-color:var(--w-e-toolbar-active-color);border-top-color:transparent;left:100%;margin-left:-10px;top:16px}.w-e-bar-item-group .w-e-bar-item-menus-container{background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 10px #0000001f;display:none;left:0;margin-top:40px;position:absolute;top:0;z-index:1}.w-e-bar-item-group:hover .w-e-bar-item-menus-container{display:block}.w-e-select-list{background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 10px #0000001f;left:0;margin-top:40px;max-height:350px;min-width:100px;overflow-y:auto;position:absolute;top:0;z-index:1}.w-e-select-list ul{line-height:1;list-style:none}.w-e-select-list ul .selected{background-color:var(--w-e-toolbar-active-bg-color)}.w-e-select-list ul li{cursor:pointer;padding:7px 0 7px 25px;position:relative;text-align:left;white-space:nowrap}.w-e-select-list ul li:hover{background-color:var(--w-e-toolbar-active-bg-color)}.w-e-select-list ul li svg{left:0;margin-left:5px;margin-top:-7px;position:absolute;top:50%}.w-e-bar-bottom .w-e-select-list{bottom:0;margin-bottom:40px;margin-top:0;top:inherit}.w-e-drop-panel{background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 10px #0000001f;margin-top:40px;min-width:200px;padding:10px;position:absolute;top:0;z-index:1}.w-e-bar-bottom .w-e-drop-panel{bottom:0;margin-bottom:40px;margin-top:0;top:inherit}.w-e-modal{background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 10px #0000001f;color:var(--w-e-toolbar-color);font-size:14px;min-height:40px;min-width:100px;padding:20px 15px 0;position:absolute;text-align:left;z-index:1}.w-e-modal .btn-close{cursor:pointer;line-height:1;padding:5px;position:absolute;right:8px;top:7px}.w-e-modal .btn-close svg{fill:var(--w-e-toolbar-color);height:10px;width:10px}.w-e-modal .babel-container{display:block;margin-bottom:15px}.w-e-modal .babel-container span{display:block;margin-bottom:10px}.w-e-modal .button-container{margin-bottom:15px}.w-e-modal button{background-color:var(--w-e-modal-button-bg-color);border:1px solid var(--w-e-modal-button-border-color);border-radius:4px;color:var(--w-e-toolbar-color);cursor:pointer;font-weight:400;height:32px;padding:4.5px 15px;text-align:center;touch-action:manipulation;transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.w-e-modal input[type=number],.w-e-modal input[type=text],.w-e-modal textarea{font-feature-settings:"tnum";background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-modal-button-border-color);border-radius:4px;color:var(--w-e-toolbar-color);font-variant:tabular-nums;padding:4.5px 11px;transition:all .3s;width:100%}.w-e-modal textarea{min-height:60px}body .w-e-modal,body .w-e-modal *{box-sizing:border-box}.w-e-progress-bar{background-color:var(--w-e-textarea-handler-bg-color);height:1px;position:absolute;transition:width .3s;width:0}.w-e-full-screen-container{bottom:0!important;display:flex!important;flex-direction:column!important;height:100%!important;left:0!important;margin:0!important;padding:0!important;position:fixed;right:0!important;top:0!important;width:100%!important}.w-e-full-screen-container [data-w-e-textarea=true]{flex:1!important}.w-e-text-container [data-slate-editor] code{background-color:var(--w-e-textarea-slight-bg-color);border-radius:3px;font-family:monospace;padding:3px}.w-e-panel-content-color{list-style:none;text-align:left;width:230px}.w-e-panel-content-color li{border:1px solid var(--w-e-toolbar-bg-color);border-radius:3px;cursor:pointer;display:inline-block;padding:2px}.w-e-panel-content-color li:hover{border-color:var(--w-e-toolbar-color)}.w-e-panel-content-color li .color-block{border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;height:17px;width:17px}.w-e-panel-content-color .active{border-color:var(--w-e-toolbar-color)}.w-e-panel-content-color .clear{line-height:1.5;margin-bottom:5px;width:100%}.w-e-panel-content-color .clear svg{height:16px;margin-bottom:-4px;width:16px}.w-e-text-container [data-slate-editor] blockquote{background-color:var(--w-e-textarea-slight-bg-color);border-left:8px solid var(--w-e-textarea-selected-border-color);display:block;font-size:100%;line-height:1.5;margin:10px 0;padding:10px}.w-e-panel-content-emotion{font-size:20px;list-style:none;text-align:left;width:300px}.w-e-panel-content-emotion li{border-radius:3px;cursor:pointer;display:inline-block;padding:0 5px}.w-e-panel-content-emotion li:hover{background-color:var(--w-e-textarea-slight-bg-color)}.w-e-textarea-divider{border-radius:3px;margin:20px auto;padding:20px}.w-e-textarea-divider hr{background-color:var(--w-e-textarea-border-color);border:0;display:block;height:1px}.w-e-text-container [data-slate-editor] pre>code{background-color:var(--w-e-textarea-slight-bg-color);border:1px solid var(--w-e-textarea-slight-border-color);border-radius:4px;display:block;font-size:14px;padding:10px;text-indent:0}.w-e-text-container [data-slate-editor] .w-e-image-container{display:inline-block;margin:0 3px}.w-e-text-container [data-slate-editor] .w-e-image-container:hover{box-shadow:0 0 0 2px var(--w-e-textarea-selected-border-color)}.w-e-text-container [data-slate-editor] .w-e-selected-image-container{overflow:hidden;position:relative}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .w-e-image-dragger{background-color:var(--w-e-textarea-handler-bg-color);height:7px;position:absolute;width:7px}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .left-top{cursor:nwse-resize;left:0;top:0}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .right-top{cursor:nesw-resize;right:0;top:0}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .left-bottom{bottom:0;cursor:nesw-resize;left:0}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .right-bottom{bottom:0;cursor:nwse-resize;right:0}.w-e-text-container [data-slate-editor] .w-e-selected-image-container:hover,.w-e-text-container [contenteditable=false] .w-e-image-container:hover{box-shadow:none}.w-e-text-container [data-slate-editor] .table-container{border:1px dashed var(--w-e-textarea-border-color);border-radius:5px;margin-top:10px;overflow-x:auto;padding:10px;width:100%}.w-e-text-container [data-slate-editor] table{border-collapse:collapse}.w-e-text-container [data-slate-editor] table td,.w-e-text-container [data-slate-editor] table th{border:1px solid var(--w-e-textarea-border-color);line-height:1.5;min-width:30px;padding:3px 5px;text-align:left}.w-e-text-container [data-slate-editor] table th{background-color:var(--w-e-textarea-slight-bg-color);font-weight:700;text-align:center}.w-e-panel-content-table{background-color:var(--w-e-toolbar-bg-color)}.w-e-panel-content-table table{border-collapse:collapse}.w-e-panel-content-table td{border:1px solid var(--w-e-toolbar-border-color);cursor:pointer;height:15px;padding:3px 5px;width:20px}.w-e-panel-content-table td.active{background-color:var(--w-e-toolbar-active-bg-color)}.w-e-textarea-video-container{background-image:linear-gradient(45deg,#eee 25%,transparent 0,transparent 75%,#eee 0,#eee),linear-gradient(45deg,#eee 25%,#fff 0,#fff 75%,#eee 0,#eee);background-position:0 0,10px 10px;background-size:20px 20px;border:1px dashed var(--w-e-textarea-border-color);border-radius:5px;margin:10px auto 0;padding:10px 0;text-align:center}.w-e-text-container [data-slate-editor] pre>code{word-wrap:normal;font-family:Consolas,Monaco,Andale Mono,Ubuntu Mono,monospace;-webkit-hyphens:none;hyphens:none;line-height:1.5;margin:.5em 0;overflow:auto;padding:1em;-moz-tab-size:4;-o-tab-size:4;tab-size:4;text-align:left;text-shadow:0 1px #fff;white-space:pre;word-break:normal;word-spacing:normal}.w-e-text-container [data-slate-editor] pre>code .token.cdata,.w-e-text-container [data-slate-editor] pre>code .token.comment,.w-e-text-container [data-slate-editor] pre>code .token.doctype,.w-e-text-container [data-slate-editor] pre>code .token.prolog{color:#708090}.w-e-text-container [data-slate-editor] pre>code .token.punctuation{color:#999}.w-e-text-container [data-slate-editor] pre>code .token.namespace{opacity:.7}.w-e-text-container [data-slate-editor] pre>code .token.boolean,.w-e-text-container [data-slate-editor] pre>code .token.constant,.w-e-text-container [data-slate-editor] pre>code .token.deleted,.w-e-text-container [data-slate-editor] pre>code .token.number,.w-e-text-container [data-slate-editor] pre>code .token.property,.w-e-text-container [data-slate-editor] pre>code .token.symbol,.w-e-text-container [data-slate-editor] pre>code .token.tag{color:#905}.w-e-text-container [data-slate-editor] pre>code .token.attr-name,.w-e-text-container [data-slate-editor] pre>code .token.builtin,.w-e-text-container [data-slate-editor] pre>code .token.char,.w-e-text-container [data-slate-editor] pre>code .token.inserted,.w-e-text-container [data-slate-editor] pre>code .token.selector,.w-e-text-container [data-slate-editor] pre>code .token.string{color:#690}.w-e-text-container [data-slate-editor] pre>code .language-css .token.string,.w-e-text-container [data-slate-editor] pre>code .style .token.string,.w-e-text-container [data-slate-editor] pre>code .token.entity,.w-e-text-container [data-slate-editor] pre>code .token.operator,.w-e-text-container [data-slate-editor] pre>code .token.url{color:#9a6e3a}.w-e-text-container [data-slate-editor] pre>code .token.atrule,.w-e-text-container [data-slate-editor] pre>code .token.attr-value,.w-e-text-container [data-slate-editor] pre>code .token.keyword{color:#07a}.w-e-text-container [data-slate-editor] pre>code .token.class-name,.w-e-text-container [data-slate-editor] pre>code .token.function{color:#dd4a68}.w-e-text-container [data-slate-editor] pre>code .token.important,.w-e-text-container [data-slate-editor] pre>code .token.regex,.w-e-text-container [data-slate-editor] pre>code .token.variable{color:#e90}.w-e-text-container [data-slate-editor] pre>code .token.bold,.w-e-text-container [data-slate-editor] pre>code .token.important{font-weight:700}.w-e-text-container [data-slate-editor] pre>code .token.italic{font-style:italic}.w-e-text-container [data-slate-editor] pre>code .token.entity{cursor:help} diff --git a/backend/platform-system/src/main/resources/static/assets/editor-b13c93a6.js b/backend/platform-system/src/main/resources/static/assets/editor-b13c93a6.js new file mode 100644 index 0000000..65ea8e0 --- /dev/null +++ b/backend/platform-system/src/main/resources/static/assets/editor-b13c93a6.js @@ -0,0 +1,186 @@ +import{d as N1,r as Eg,al as lP,k as u$,a2 as s$,o as I1,c as L1,a8 as l$,am as c$,a9 as cP,aa as fP,K as f$,g as Dg,a as $r,Q as d$,ad as yx,w as p$}from"./index-b25f0d08.js";import{u as h$}from"./index-5282e30f.js";var se=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function g$(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}function us(t){var e={exports:{}};return t(e,e.exports),e.exports}var yi,$0,Fh=function(t){return t&&t.Math==Math&&t},kt=Fh(typeof globalThis=="object"&&globalThis)||Fh(typeof window=="object"&&window)||Fh(typeof self=="object"&&self)||Fh(typeof se=="object"&&se)||function(){return this}()||Function("return this")(),r5=Function.prototype,bx=r5.apply,v$=r5.bind,wx=r5.call,dP=typeof Reflect=="object"&&Reflect.apply||(v$?wx.bind(bx):function(){return wx.apply(bx,arguments)}),pP=Function.prototype,w4=pP.bind,E4=pP.call,m$=w4&&w4.bind(E4),ge=w4?function(t){return t&&m$(E4,t)}:function(t){return t&&function(){return E4.apply(t,arguments)}},sn=function(t){return typeof t=="function"},Gn=function(t){try{return!!t()}catch{return!0}},Hn=!Gn(function(){return Object.defineProperty({},1,{get:function(){return 7}})[1]!=7}),kc=Function.prototype.call,zn=kc.bind?kc.bind(kc):function(){return kc.apply(kc,arguments)},Ex={}.propertyIsEnumerable,Dx=Object.getOwnPropertyDescriptor,y$=Dx&&!Ex.call({1:2},1)?function(t){var e=Dx(this,t);return!!e&&e.enumerable}:Ex,o5={f:y$},Xr=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}},b$=ge({}.toString),w$=ge("".slice),Du=function(t){return w$(b$(t),8,-1)},qm=kt.Object,E$=ge("".split),hP=Gn(function(){return!qm("z").propertyIsEnumerable(0)})?function(t){return Du(t)=="String"?E$(t,""):qm(t)}:qm,D$=kt.TypeError,i5=function(t){if(t==null)throw D$("Can't call method on "+t);return t},jo=function(t){return hP(i5(t))},tr=function(t){return typeof t=="object"?t!==null:sn(t)},Qn={},Cx=function(t){return sn(t)?t:void 0},oc=function(t,e){return arguments.length<2?Cx(Qn[t])||Cx(kt[t]):Qn[t]&&Qn[t][e]||kt[t]&&kt[t][e]},Td=ge({}.isPrototypeOf),Km=oc("navigator","userAgent")||"",xx=kt.process,Sx=kt.Deno,Ax=xx&&xx.versions||Sx&&Sx.version,Ox=Ax&&Ax.v8;Ox&&($0=(yi=Ox.split("."))[0]>0&&yi[0]<4?1:+(yi[0]+yi[1])),!$0&&Km&&(!(yi=Km.match(/Edge\/(\d+)/))||yi[1]>=74)&&(yi=Km.match(/Chrome\/(\d+)/))&&($0=+yi[1]);var _h,Cg=$0,ho=!!Object.getOwnPropertySymbols&&!Gn(function(){var t=Symbol();return!String(t)||!(Object(t)instanceof Symbol)||!Symbol.sham&&Cg&&Cg<41}),a5=ho&&!Symbol.sham&&typeof Symbol.iterator=="symbol",C$=kt.Object,hl=a5?function(t){return typeof t=="symbol"}:function(t){var e=oc("Symbol");return sn(e)&&Td(e.prototype,C$(t))},x$=kt.String,D4=function(t){try{return x$(t)}catch{return"Object"}},S$=kt.TypeError,u5=function(t){if(sn(t))return t;throw S$(D4(t)+" is not a function")},xg=function(t,e){var n=t[e];return n==null?void 0:u5(n)},A$=kt.TypeError,O$=Object.defineProperty,ea=kt["__core-js_shared__"]||function(t,e){try{O$(kt,t,{value:e,configurable:!0,writable:!0})}catch{kt[t]=e}return e}("__core-js_shared__",{}),ss=us(function(t){(t.exports=function(e,n){return ea[e]||(ea[e]=n!==void 0?n:{})})("versions",[]).push({version:"3.19.3",mode:"pure",copyright:"© 2021 Denis Pushkarev (zloirock.ru)"})}),k$=kt.Object,Fp=function(t){return k$(i5(t))},B$=ge({}.hasOwnProperty),Vt=Object.hasOwn||function(t,e){return B$(Fp(t),e)},F$=0,_$=Math.random(),T$=ge(1 .toString),Sg=function(t){return"Symbol("+(t===void 0?"":t)+")_"+T$(++F$+_$,36)},Bc=ss("wks"),Cu=kt.Symbol,kx=Cu&&Cu.for,P$=a5?Cu:Cu&&Cu.withoutSetter||Sg,Bn=function(t){if(!Vt(Bc,t)||!ho&&typeof Bc[t]!="string"){var e="Symbol."+t;ho&&Vt(Cu,t)?Bc[t]=Cu[t]:Bc[t]=a5&&kx?kx(e):P$(e)}return Bc[t]},j$=kt.TypeError,N$=Bn("toPrimitive"),I$=function(t,e){if(!tr(t)||hl(t))return t;var n,r=xg(t,N$);if(r){if(e===void 0&&(e="default"),n=zn(r,t,e),!tr(n)||hl(n))return n;throw j$("Can't convert object to primitive value")}return e===void 0&&(e="number"),function(o,i){var a,u;if(i==="string"&&sn(a=o.toString)&&!tr(u=zn(a,o))||sn(a=o.valueOf)&&!tr(u=zn(a,o))||i!=="string"&&sn(a=o.toString)&&!tr(u=zn(a,o)))return u;throw A$("Can't convert object to primitive value")}(t,e)},ic=function(t){var e=I$(t,"string");return hl(e)?e:e+""},C4=kt.document,L$=tr(C4)&&tr(C4.createElement),gP=function(t){return L$?C4.createElement(t):{}},vP=!Hn&&!Gn(function(){return Object.defineProperty(gP("div"),"a",{get:function(){return 7}}).a!=7}),Bx=Object.getOwnPropertyDescriptor,R$=Hn?Bx:function(t,e){if(t=jo(t),e=ic(e),vP)try{return Bx(t,e)}catch{}if(Vt(t,e))return Xr(!zn(o5.f,t,e),t[e])},R1={f:R$},M$=/#|\.prototype\./,_p=function(t,e){var n=$$[z$(t)];return n==V$||n!=H$&&(sn(e)?Gn(e):!!e)},z$=_p.normalize=function(t){return String(t).replace(M$,".").toLowerCase()},$$=_p.data={},H$=_p.NATIVE="N",V$=_p.POLYFILL="P",U$=_p,Fx=ge(ge.bind),s5=function(t,e){return u5(t),e===void 0?t:Fx?Fx(t,e):function(){return t.apply(e,arguments)}},W$=kt.String,G$=kt.TypeError,ar=function(t){if(tr(t))return t;throw G$(W$(t)+" is not an object")},q$=kt.TypeError,_x=Object.defineProperty,K$=Hn?_x:function(t,e,n){if(ar(t),e=ic(e),ar(n),vP)try{return _x(t,e,n)}catch{}if("get"in n||"set"in n)throw q$("Accessors not supported");return"value"in n&&(t[e]=n.value),t},Na={f:K$},Tn=Hn?function(t,e,n){return Na.f(t,e,Xr(1,n))}:function(t,e,n){return t[e]=n,t},Y$=R1.f,X$=function(t){var e=function(n,r,o){if(this instanceof e){switch(arguments.length){case 0:return new t;case 1:return new t(n);case 2:return new t(n,r)}return new t(n,r,o)}return dP(t,this,arguments)};return e.prototype=t.prototype,e},Ko=function(t,e){var n,r,o,i,a,u,s,l,c=t.target,f=t.global,p=t.stat,d=t.proto,v=f?kt:p?kt[c]:(kt[c]||{}).prototype,g=f?Qn:Qn[c]||Tn(Qn,c,{})[c],m=g.prototype;for(o in e)n=!U$(f?o:c+(p?".":"#")+o,t.forced)&&v&&Vt(v,o),a=g[o],n&&(u=t.noTargetGet?(l=Y$(v,o))&&l.value:v[o]),i=n&&u?u:e[o],n&&typeof a==typeof i||(s=t.bind&&n?s5(i,kt):t.wrap&&n?X$(i):d&&sn(i)?ge(i):i,(t.sham||i&&i.sham||a&&a.sham)&&Tn(s,"sham",!0),Tn(g,o,s),d&&(Vt(Qn,r=c+"Prototype")||Tn(Qn,r,{}),Tn(Qn[r],o,i),t.real&&m&&!m[o]&&Tn(m,o,i)))},Tx=ss("keys"),M1=function(t){return Tx[t]||(Tx[t]=Sg(t))},Z$=!Gn(function(){function t(){}return t.prototype.constructor=null,Object.getPrototypeOf(new t)!==t.prototype}),Px=M1("IE_PROTO"),x4=kt.Object,J$=x4.prototype,Ag=Z$?x4.getPrototypeOf:function(t){var e=Fp(t);if(Vt(e,Px))return e[Px];var n=e.constructor;return sn(n)&&e instanceof n?n.prototype:e instanceof x4?J$:null},Q$=kt.String,tH=kt.TypeError,Og=Object.setPrototypeOf||("__proto__"in{}?function(){var t,e=!1,n={};try{(t=ge(Object.getOwnPropertyDescriptor(Object.prototype,"__proto__").set))(n,[]),e=n instanceof Array}catch{}return function(r,o){return ar(r),function(i){if(typeof i=="object"||sn(i))return i;throw tH("Can't set "+Q$(i)+" as a prototype")}(o),e?t(r,o):r.__proto__=o,r}}():void 0),eH=Math.ceil,nH=Math.floor,l5=function(t){var e=+t;return e!=e||e===0?0:(e>0?nH:eH)(e)},rH=Math.max,oH=Math.min,S4=function(t,e){var n=l5(t);return n<0?rH(n+e,0):oH(n,e)},iH=Math.min,Tp=function(t){return(e=t.length)>0?iH(l5(e),9007199254740991):0;var e},jx=function(t){return function(e,n,r){var o,i=jo(e),a=Tp(i),u=S4(r,a);if(t&&n!=n){for(;a>u;)if((o=i[u++])!=o)return!0}else for(;a>u;u++)if((t||u in i)&&i[u]===n)return t||u||0;return!t&&-1}},aH={includes:jx(!0),indexOf:jx(!1)},Pp={},uH=aH.indexOf,Nx=ge([].push),mP=function(t,e){var n,r=jo(t),o=0,i=[];for(n in r)!Vt(Pp,n)&&Vt(r,n)&&Nx(i,n);for(;e.length>o;)Vt(r,n=e[o++])&&(~uH(i,n)||Nx(i,n));return i},kg=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],sH=kg.concat("length","prototype"),lH=Object.getOwnPropertyNames||function(t){return mP(t,sH)},c5={f:lH},H0={f:Object.getOwnPropertySymbols},cH=ge([].concat),fH=oc("Reflect","ownKeys")||function(t){var e=c5.f(ar(t)),n=H0.f;return n?cH(e,n(t)):e},f5=Object.keys||function(t){return mP(t,kg)},dH=Hn?Object.defineProperties:function(t,e){ar(t);for(var n,r=jo(e),o=f5(e),i=o.length,a=0;i>a;)Na.f(t,n=o[a++],r[n]);return t},pH=oc("document","documentElement"),yP=M1("IE_PROTO"),Ym=function(){},bP=function(t){return" + + + +
+ + + diff --git a/backend/qgc-eng-server/pom.xml b/backend/qgc-eng-server/pom.xml new file mode 100644 index 0000000..c9dd875 --- /dev/null +++ b/backend/qgc-eng-server/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + + com.yfd + platform + 1.0 + ../pom.xml + + + qgc-eng-server + 1.0 + jar + + QGC Eng Server + QGC Engineering Server Module + + + + + com.yfd + platform-common + 1.0 + + + + + com.yfd + platform-system + 1.0 + + + + + com.baomidou + mybatis-plus-spring-boot4-starter + + + + \ No newline at end of file diff --git a/backend/qgc-eng-server/src/main/java/com/yfd/platform/eng/controller/EngController.java b/backend/qgc-eng-server/src/main/java/com/yfd/platform/eng/controller/EngController.java new file mode 100644 index 0000000..76576d9 --- /dev/null +++ b/backend/qgc-eng-server/src/main/java/com/yfd/platform/eng/controller/EngController.java @@ -0,0 +1,47 @@ +package com.yfd.platform.eng.controller; + +import com.yfd.platform.common.response.ResponseResult; +import com.yfd.platform.eng.service.IEngService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.*; + +import jakarta.annotation.Resource; +import java.util.Map; + +/** + * 工程控制器 + */ +@RestController +@RequestMapping("/eng") +@Tag(name = "工程管理") +public class EngController { + + @Resource + private IEngService engService; + + @PostMapping("/getEngData") + @Operation(summary = "获取工程数据") + public ResponseResult getEngData(@RequestBody Map params) { + return ResponseResult.successData(engService.getEngData(params)); + } + + @PostMapping("/addEngData") + @Operation(summary = "添加工程数据") + public ResponseResult addEngData(@RequestBody Map params) { + return ResponseResult.successData(engService.addEngData(params)); + } + + @PostMapping("/updateEngData") + @Operation(summary = "更新工程数据") + public ResponseResult updateEngData(@RequestBody Map params) { + return ResponseResult.successData(engService.updateEngData(params)); + } + + @PostMapping("/deleteEngData") + @Operation(summary = "删除工程数据") + public ResponseResult deleteEngData(@RequestBody Map params) { + engService.deleteEngData(params); + return ResponseResult.success(); + } +} diff --git a/backend/qgc-eng-server/src/main/java/com/yfd/platform/eng/service/IEngService.java b/backend/qgc-eng-server/src/main/java/com/yfd/platform/eng/service/IEngService.java new file mode 100644 index 0000000..95a9b66 --- /dev/null +++ b/backend/qgc-eng-server/src/main/java/com/yfd/platform/eng/service/IEngService.java @@ -0,0 +1,35 @@ +package com.yfd.platform.eng.service; + +import java.util.Map; + +/** + * 工程服务接口 + */ +public interface IEngService { + /** + * 获取工程数据 + * @param params 参数 + * @return 工程数据 + */ + Map getEngData(Map params); + + /** + * 添加工程数据 + * @param params 参数 + * @return 结果 + */ + Map addEngData(Map params); + + /** + * 更新工程数据 + * @param params 参数 + * @return 结果 + */ + Map updateEngData(Map params); + + /** + * 删除工程数据 + * @param params 参数 + */ + void deleteEngData(Map params); +} diff --git a/backend/qgc-eng-server/src/main/java/com/yfd/platform/eng/service/impl/EngServiceImpl.java b/backend/qgc-eng-server/src/main/java/com/yfd/platform/eng/service/impl/EngServiceImpl.java new file mode 100644 index 0000000..babc2ff --- /dev/null +++ b/backend/qgc-eng-server/src/main/java/com/yfd/platform/eng/service/impl/EngServiceImpl.java @@ -0,0 +1,43 @@ +package com.yfd.platform.eng.service.impl; + +import com.yfd.platform.eng.service.IEngService; +import java.util.HashMap; +import java.util.Map; + +/** + * 工程服务实现类 + */ +public class EngServiceImpl implements IEngService { + + @Override + public Map getEngData(Map params) { + Map result = new HashMap<>(); + // TODO: 实现获取工程数据逻辑 + result.put("total", 0); + result.put("data", null); + return result; + } + + @Override + public Map addEngData(Map params) { + Map result = new HashMap<>(); + // TODO: 实现添加工程数据逻辑 + result.put("success", true); + result.put("message", "工程数据添加成功"); + return result; + } + + @Override + public Map updateEngData(Map params) { + Map result = new HashMap<>(); + // TODO: 实现更新工程数据逻辑 + result.put("success", true); + result.put("message", "工程数据更新成功"); + return result; + } + + @Override + public void deleteEngData(Map params) { + // TODO: 实现删除工程数据逻辑 + } +} diff --git a/backend/qgc-env-server/pom.xml b/backend/qgc-env-server/pom.xml new file mode 100644 index 0000000..1ee4d6b --- /dev/null +++ b/backend/qgc-env-server/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + + com.yfd + platform + 1.0 + ../pom.xml + + + qgc-env-server + 1.0 + jar + + QGC Env Server + QGC Environment Server Module + + + + + com.yfd + platform-common + 1.0 + + + + + com.yfd + platform-system + 1.0 + + + + + com.baomidou + mybatis-plus-spring-boot4-starter + + + + \ No newline at end of file diff --git a/backend/qgc-env-server/src/main/java/com/yfd/platform/env/controller/EnvController.java b/backend/qgc-env-server/src/main/java/com/yfd/platform/env/controller/EnvController.java new file mode 100644 index 0000000..a7623ed --- /dev/null +++ b/backend/qgc-env-server/src/main/java/com/yfd/platform/env/controller/EnvController.java @@ -0,0 +1,47 @@ +package com.yfd.platform.env.controller; + +import com.yfd.platform.common.response.ResponseResult; +import com.yfd.platform.env.service.IEnvService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.*; + +import jakarta.annotation.Resource; +import java.util.Map; + +/** + * 环境控制器 + */ +@RestController +@RequestMapping("/env") +@Tag(name = "环境管理") +public class EnvController { + + @Resource + private IEnvService envService; + + @PostMapping("/getEnvData") + @Operation(summary = "获取环境数据") + public ResponseResult getEnvData(@RequestBody Map params) { + return ResponseResult.successData(envService.getEnvData(params)); + } + + @PostMapping("/addEnvData") + @Operation(summary = "添加环境数据") + public ResponseResult addEnvData(@RequestBody Map params) { + return ResponseResult.successData(envService.addEnvData(params)); + } + + @PostMapping("/updateEnvData") + @Operation(summary = "更新环境数据") + public ResponseResult updateEnvData(@RequestBody Map params) { + return ResponseResult.successData(envService.updateEnvData(params)); + } + + @PostMapping("/deleteEnvData") + @Operation(summary = "删除环境数据") + public ResponseResult deleteEnvData(@RequestBody Map params) { + envService.deleteEnvData(params); + return ResponseResult.success(); + } +} diff --git a/backend/qgc-env-server/src/main/java/com/yfd/platform/env/service/IEnvService.java b/backend/qgc-env-server/src/main/java/com/yfd/platform/env/service/IEnvService.java new file mode 100644 index 0000000..25a85a9 --- /dev/null +++ b/backend/qgc-env-server/src/main/java/com/yfd/platform/env/service/IEnvService.java @@ -0,0 +1,35 @@ +package com.yfd.platform.env.service; + +import java.util.Map; + +/** + * 环境服务接口 + */ +public interface IEnvService { + /** + * 获取环境数据 + * @param params 参数 + * @return 环境数据 + */ + Map getEnvData(Map params); + + /** + * 添加环境数据 + * @param params 参数 + * @return 结果 + */ + Map addEnvData(Map params); + + /** + * 更新环境数据 + * @param params 参数 + * @return 结果 + */ + Map updateEnvData(Map params); + + /** + * 删除环境数据 + * @param params 参数 + */ + void deleteEnvData(Map params); +} diff --git a/backend/qgc-env-server/src/main/java/com/yfd/platform/env/service/impl/EnvServiceImpl.java b/backend/qgc-env-server/src/main/java/com/yfd/platform/env/service/impl/EnvServiceImpl.java new file mode 100644 index 0000000..c22c5d3 --- /dev/null +++ b/backend/qgc-env-server/src/main/java/com/yfd/platform/env/service/impl/EnvServiceImpl.java @@ -0,0 +1,43 @@ +package com.yfd.platform.env.service.impl; + +import com.yfd.platform.env.service.IEnvService; +import java.util.HashMap; +import java.util.Map; + +/** + * 环境服务实现类 + */ +public class EnvServiceImpl implements IEnvService { + + @Override + public Map getEnvData(Map params) { + Map result = new HashMap<>(); + // TODO: 实现获取环境数据逻辑 + result.put("total", 0); + result.put("data", null); + return result; + } + + @Override + public Map addEnvData(Map params) { + Map result = new HashMap<>(); + // TODO: 实现添加环境数据逻辑 + result.put("success", true); + result.put("message", "环境数据添加成功"); + return result; + } + + @Override + public Map updateEnvData(Map params) { + Map result = new HashMap<>(); + // TODO: 实现更新环境数据逻辑 + result.put("success", true); + result.put("message", "环境数据更新成功"); + return result; + } + + @Override + public void deleteEnvData(Map params) { + // TODO: 实现删除环境数据逻辑 + } +}