From ae43c27cca0d4a7a4b2d6499a1288f78fe5d3097 Mon Sep 17 00:00:00 2001 From: zzl <961867786@qq.com> Date: Wed, 25 Jun 2025 14:32:56 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=93=E5=BA=93=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/clean.bat | 12 + bin/package.bat | 12 + bin/run.bat | 14 + doc/若依环境使用手册.docx | Bin 0 -> 428152 bytes pom.xml | 281 +++ ruoyi-admin/pom.xml | 96 + ruoyi-admin/ruoyi-admin.iml | 159 ++ .../main/java/com/ruoyi/RuoYiApplication.java | 30 + .../com/ruoyi/RuoYiServletInitializer.java | 18 + .../controller/common/CaptchaController.java | 94 + .../controller/common/CommonController.java | 162 ++ .../controller/monitor/CacheController.java | 121 ++ .../controller/monitor/ServerController.java | 27 + .../monitor/SysLogininforController.java | 82 + .../monitor/SysOperlogController.java | 69 + .../monitor/SysUserOnlineController.java | 83 + .../open/SysConfigOpenController.java | 52 + .../system/SysConfigController.java | 133 ++ .../controller/system/SysDeptController.java | 132 ++ .../system/SysDictDataController.java | 121 ++ .../system/SysDictTypeController.java | 131 ++ .../controller/system/SysIndexController.java | 29 + .../controller/system/SysLoginController.java | 131 ++ .../controller/system/SysMenuController.java | 142 ++ .../system/SysNoticeController.java | 91 + .../controller/system/SysPostController.java | 129 ++ .../system/SysProfileController.java | 148 ++ .../system/SysRegisterController.java | 38 + .../controller/system/SysRoleController.java | 262 +++ .../controller/system/SysUserController.java | 256 +++ .../web/controller/tool/TestController.java | 183 ++ .../ruoyi/web/core/config/SwaggerConfig.java | 125 ++ .../META-INF/spring-devtools.properties | 1 + .../src/main/resources/application-druid.yml | 61 + .../src/main/resources/application-third.yml | 0 .../src/main/resources/application.yml | 131 ++ ruoyi-admin/src/main/resources/banner.txt | 24 + .../main/resources/i18n/messages.properties | 38 + ruoyi-admin/src/main/resources/logback.xml | 93 + .../main/resources/mybatis/mybatis-config.xml | 20 + ruoyi-business/pom.xml | 15 + ruoyi-common/pom.xml | 124 ++ ruoyi-common/ruoyi-common.iml | 98 + .../ruoyi/common/annotation/Anonymous.java | 19 + .../ruoyi/common/annotation/DataScope.java | 33 + .../ruoyi/common/annotation/DataSource.java | 28 + .../com/ruoyi/common/annotation/Excel.java | 197 ++ .../com/ruoyi/common/annotation/Excels.java | 18 + .../java/com/ruoyi/common/annotation/Log.java | 51 + .../ruoyi/common/annotation/RateLimiter.java | 40 + .../ruoyi/common/annotation/RepeatSubmit.java | 31 + .../ruoyi/common/annotation/Sensitive.java | 24 + .../com/ruoyi/common/config/RuoYiConfig.java | 122 ++ .../serializer/SensitiveJsonSerializer.java | 67 + .../ruoyi/common/constant/CacheConstants.java | 44 + .../com/ruoyi/common/constant/Constants.java | 173 ++ .../ruoyi/common/constant/GenConstants.java | 117 + .../com/ruoyi/common/constant/HttpStatus.java | 94 + .../common/constant/ScheduleConstants.java | 50 + .../ruoyi/common/constant/UserConstants.java | 81 + .../core/controller/BaseController.java | 202 ++ .../ruoyi/common/core/domain/AjaxResult.java | 216 ++ .../ruoyi/common/core/domain/BaseEntity.java | 118 + .../java/com/ruoyi/common/core/domain/R.java | 115 + .../ruoyi/common/core/domain/TreeEntity.java | 79 + .../ruoyi/common/core/domain/TreeSelect.java | 93 + .../common/core/domain/entity/SysDept.java | 203 ++ .../core/domain/entity/SysDictData.java | 176 ++ .../core/domain/entity/SysDictType.java | 96 + .../common/core/domain/entity/SysMenu.java | 274 +++ .../common/core/domain/entity/SysRole.java | 241 +++ .../common/core/domain/entity/SysUser.java | 338 +++ .../common/core/domain/model/LoginBody.java | 69 + .../common/core/domain/model/LoginUser.java | 266 +++ .../core/domain/model/RegisterBody.java | 11 + .../ruoyi/common/core/page/PageDomain.java | 101 + .../ruoyi/common/core/page/TableDataInfo.java | 85 + .../ruoyi/common/core/page/TableSupport.java | 56 + .../ruoyi/common/core/redis/RedisCache.java | 268 +++ .../ruoyi/common/core/text/CharsetKit.java | 86 + .../com/ruoyi/common/core/text/Convert.java | 1018 +++++++++ .../ruoyi/common/core/text/StrFormatter.java | 92 + .../ruoyi/common/enums/BusinessStatus.java | 20 + .../com/ruoyi/common/enums/BusinessType.java | 59 + .../ruoyi/common/enums/DataSourceType.java | 19 + .../ruoyi/common/enums/DesensitizedType.java | 59 + .../com/ruoyi/common/enums/HttpMethod.java | 36 + .../com/ruoyi/common/enums/LimitType.java | 20 + .../com/ruoyi/common/enums/OperatorType.java | 24 + .../com/ruoyi/common/enums/UserStatus.java | 30 + .../common/exception/DemoModeException.java | 15 + .../common/exception/GlobalException.java | 58 + .../common/exception/ServiceException.java | 74 + .../ruoyi/common/exception/UtilException.java | 26 + .../common/exception/base/BaseException.java | 97 + .../common/exception/file/FileException.java | 19 + .../FileNameLengthLimitExceededException.java | 16 + .../file/FileSizeLimitExceededException.java | 16 + .../exception/file/FileUploadException.java | 61 + .../file/InvalidExtensionException.java | 80 + .../common/exception/job/TaskException.java | 34 + .../exception/user/BlackListException.java | 16 + .../exception/user/CaptchaException.java | 16 + .../user/CaptchaExpireException.java | 16 + .../common/exception/user/UserException.java | 18 + .../user/UserNotExistsException.java | 16 + .../user/UserPasswordNotMatchException.java | 16 + ...UserPasswordRetryLimitExceedException.java | 16 + .../filter/PropertyPreExcludeFilter.java | 24 + .../ruoyi/common/filter/RepeatableFilter.java | 52 + .../filter/RepeatedlyRequestWrapper.java | 76 + .../com/ruoyi/common/filter/XssFilter.java | 75 + .../filter/XssHttpServletRequestWrapper.java | 111 + .../java/com/ruoyi/common/utils/Arith.java | 113 + .../com/ruoyi/common/utils/DateUtils.java | 191 ++ .../ruoyi/common/utils/DesensitizedUtil.java | 49 + .../com/ruoyi/common/utils/DictUtils.java | 239 +++ .../com/ruoyi/common/utils/ExceptionUtil.java | 39 + .../java/com/ruoyi/common/utils/LogUtils.java | 18 + .../com/ruoyi/common/utils/MessageUtils.java | 26 + .../com/ruoyi/common/utils/PageUtils.java | 35 + .../com/ruoyi/common/utils/SecurityUtils.java | 178 ++ .../com/ruoyi/common/utils/ServletUtils.java | 218 ++ .../com/ruoyi/common/utils/StringUtils.java | 722 +++++++ .../java/com/ruoyi/common/utils/Threads.java | 99 + .../ruoyi/common/utils/bean/BeanUtils.java | 110 + .../common/utils/bean/BeanValidators.java | 24 + .../common/utils/file/FileTypeUtils.java | 76 + .../common/utils/file/FileUploadUtils.java | 260 +++ .../ruoyi/common/utils/file/FileUtils.java | 303 +++ .../ruoyi/common/utils/file/ImageUtils.java | 98 + .../common/utils/file/MimeTypeUtils.java | 59 + .../ruoyi/common/utils/html/EscapeUtil.java | 167 ++ .../ruoyi/common/utils/html/HTMLFilter.java | 570 +++++ .../ruoyi/common/utils/http/HttpHelper.java | 55 + .../ruoyi/common/utils/http/HttpUtils.java | 293 +++ .../ruoyi/common/utils/ip/AddressUtils.java | 56 + .../com/ruoyi/common/utils/ip/IpUtils.java | 382 ++++ .../common/utils/poi/ExcelHandlerAdapter.java | 24 + .../com/ruoyi/common/utils/poi/ExcelUtil.java | 1893 +++++++++++++++++ .../common/utils/reflect/ReflectUtils.java | 410 ++++ .../com/ruoyi/common/utils/sign/Base64.java | 291 +++ .../com/ruoyi/common/utils/sign/Md5Utils.java | 67 + .../common/utils/spring/SpringUtils.java | 164 ++ .../com/ruoyi/common/utils/sql/SqlUtil.java | 70 + .../com/ruoyi/common/utils/uuid/IdUtils.java | 49 + .../java/com/ruoyi/common/utils/uuid/Seq.java | 86 + .../com/ruoyi/common/utils/uuid/UUID.java | 484 +++++ .../main/java/com/ruoyi/common/xss/Xss.java | 27 + .../com/ruoyi/common/xss/XssValidator.java | 39 + ruoyi-framework/pom.xml | 64 + ruoyi-framework/ruoyi-framework.iml | 128 ++ .../framework/aspectj/DataScopeAspect.java | 184 ++ .../framework/aspectj/DataSourceAspect.java | 72 + .../ruoyi/framework/aspectj/LogAspect.java | 256 +++ .../framework/aspectj/RateLimiterAspect.java | 89 + .../framework/config/ApplicationConfig.java | 30 + .../ruoyi/framework/config/CaptchaConfig.java | 83 + .../ruoyi/framework/config/DruidConfig.java | 126 ++ .../config/FastJson2JsonRedisSerializer.java | 52 + .../ruoyi/framework/config/FilterConfig.java | 58 + .../ruoyi/framework/config/I18nConfig.java | 43 + .../framework/config/KaptchaTextCreator.java | 68 + .../ruoyi/framework/config/MyBatisConfig.java | 132 ++ .../ruoyi/framework/config/RedisConfig.java | 69 + .../framework/config/ResourcesConfig.java | 72 + .../framework/config/SecurityConfig.java | 142 ++ .../ruoyi/framework/config/ServerConfig.java | 32 + .../framework/config/ThreadPoolConfig.java | 63 + .../config/properties/DruidProperties.java | 89 + .../properties/PermitAllUrlProperties.java | 73 + .../datasource/DynamicDataSource.java | 26 + .../DynamicDataSourceContextHolder.java | 45 + .../interceptor/RepeatSubmitInterceptor.java | 56 + .../impl/SameUrlDataInterceptor.java | 110 + .../ruoyi/framework/manager/AsyncManager.java | 55 + .../framework/manager/ShutdownManager.java | 39 + .../manager/factory/AsyncFactory.java | 102 + .../context/AuthenticationContextHolder.java | 28 + .../context/PermissionContextHolder.java | 27 + .../filter/JwtAuthenticationTokenFilter.java | 44 + .../handle/AuthenticationEntryPointImpl.java | 34 + .../handle/LogoutSuccessHandlerImpl.java | 53 + .../ruoyi/framework/web/domain/Server.java | 240 +++ .../framework/web/domain/server/Cpu.java | 101 + .../framework/web/domain/server/Jvm.java | 130 ++ .../framework/web/domain/server/Mem.java | 61 + .../framework/web/domain/server/Sys.java | 84 + .../framework/web/domain/server/SysFile.java | 114 + .../web/exception/GlobalExceptionHandler.java | 145 ++ .../web/service/PermissionService.java | 159 ++ .../web/service/SysLoginService.java | 181 ++ .../web/service/SysPasswordService.java | 86 + .../web/service/SysPermissionService.java | 88 + .../web/service/SysRegisterService.java | 117 + .../framework/web/service/TokenService.java | 232 ++ .../web/service/UserDetailsServiceImpl.java | 66 + ruoyi-generator/pom.xml | 40 + ruoyi-generator/ruoyi-generator.iml | 108 + .../com/ruoyi/generator/config/GenConfig.java | 87 + .../generator/controller/GenController.java | 263 +++ .../com/ruoyi/generator/domain/GenTable.java | 385 ++++ .../generator/domain/GenTableColumn.java | 373 ++++ .../mapper/GenTableColumnMapper.java | 60 + .../generator/mapper/GenTableMapper.java | 91 + .../service/GenTableColumnServiceImpl.java | 68 + .../service/GenTableServiceImpl.java | 531 +++++ .../service/IGenTableColumnService.java | 44 + .../generator/service/IGenTableService.java | 130 ++ .../com/ruoyi/generator/util/GenUtils.java | 257 +++ .../generator/util/VelocityInitializer.java | 34 + .../ruoyi/generator/util/VelocityUtils.java | 408 ++++ .../src/main/resources/generator.yml | 12 + .../mapper/generator/GenTableColumnMapper.xml | 127 ++ .../mapper/generator/GenTableMapper.xml | 210 ++ .../main/resources/vm/java/controller.java.vm | 197 ++ .../src/main/resources/vm/java/domain.java.vm | 105 + .../src/main/resources/vm/java/mapper.java.vm | 107 + .../main/resources/vm/java/service.java.vm | 87 + .../resources/vm/java/serviceImpl.java.vm | 280 +++ .../main/resources/vm/java/sub-domain.java.vm | 76 + .../src/main/resources/vm/js/api.js.vm | 54 + .../src/main/resources/vm/sql/sql.vm | 32 + .../main/resources/vm/vue/index-tree.vue.vm | 505 +++++ .../src/main/resources/vm/vue/index.vue.vm | 638 ++++++ .../resources/vm/vue/v3/index-tree.vue.vm | 474 +++++ .../src/main/resources/vm/vue/v3/index.vue.vm | 590 +++++ .../src/main/resources/vm/xml/mapper.xml.vm | 176 ++ ruoyi-quartz/pom.xml | 40 + ruoyi-quartz/ruoyi-quartz.iml | 102 + .../ruoyi/quartz/config/ScheduleConfig.java | 57 + .../quartz/controller/SysJobController.java | 185 ++ .../controller/SysJobLogController.java | 92 + .../java/com/ruoyi/quartz/domain/SysJob.java | 171 ++ .../com/ruoyi/quartz/domain/SysJobLog.java | 155 ++ .../ruoyi/quartz/mapper/SysJobLogMapper.java | 64 + .../com/ruoyi/quartz/mapper/SysJobMapper.java | 67 + .../quartz/service/ISysJobLogService.java | 56 + .../ruoyi/quartz/service/ISysJobService.java | 102 + .../service/impl/SysJobLogServiceImpl.java | 87 + .../service/impl/SysJobServiceImpl.java | 261 +++ .../java/com/ruoyi/quartz/task/RyTask.java | 28 + .../ruoyi/quartz/util/AbstractQuartzJob.java | 106 + .../java/com/ruoyi/quartz/util/CronUtils.java | 63 + .../com/ruoyi/quartz/util/JobInvokeUtil.java | 182 ++ .../QuartzDisallowConcurrentExecution.java | 21 + .../ruoyi/quartz/util/QuartzJobExecution.java | 19 + .../com/ruoyi/quartz/util/ScheduleUtils.java | 141 ++ .../mapper/quartz/SysJobLogMapper.xml | 94 + .../resources/mapper/quartz/SysJobMapper.xml | 111 + ruoyi-system/pom.xml | 28 + ruoyi-system/ruoyi-system.iml | 100 + .../com/ruoyi/system/domain/SysCache.java | 81 + .../com/ruoyi/system/domain/SysConfig.java | 111 + .../ruoyi/system/domain/SysLogininfor.java | 144 ++ .../com/ruoyi/system/domain/SysNotice.java | 102 + .../com/ruoyi/system/domain/SysOperLog.java | 269 +++ .../java/com/ruoyi/system/domain/SysPost.java | 124 ++ .../com/ruoyi/system/domain/SysRoleDept.java | 46 + .../com/ruoyi/system/domain/SysRoleMenu.java | 46 + .../ruoyi/system/domain/SysUserOnline.java | 113 + .../com/ruoyi/system/domain/SysUserPost.java | 46 + .../com/ruoyi/system/domain/SysUserRole.java | 46 + .../com/ruoyi/system/domain/vo/MetaVo.java | 106 + .../com/ruoyi/system/domain/vo/RouterVo.java | 148 ++ .../ruoyi/system/mapper/SysConfigMapper.java | 76 + .../ruoyi/system/mapper/SysDeptMapper.java | 118 + .../system/mapper/SysDictDataMapper.java | 95 + .../system/mapper/SysDictTypeMapper.java | 83 + .../system/mapper/SysLogininforMapper.java | 42 + .../ruoyi/system/mapper/SysMenuMapper.java | 125 ++ .../ruoyi/system/mapper/SysNoticeMapper.java | 60 + .../ruoyi/system/mapper/SysOperLogMapper.java | 48 + .../ruoyi/system/mapper/SysPostMapper.java | 99 + .../system/mapper/SysRoleDeptMapper.java | 44 + .../ruoyi/system/mapper/SysRoleMapper.java | 107 + .../system/mapper/SysRoleMenuMapper.java | 44 + .../ruoyi/system/mapper/SysUserMapper.java | 127 ++ .../system/mapper/SysUserPostMapper.java | 44 + .../system/mapper/SysUserRoleMapper.java | 62 + .../system/service/ISysConfigService.java | 90 + .../ruoyi/system/service/ISysDeptService.java | 124 ++ .../system/service/ISysDictDataService.java | 60 + .../system/service/ISysDictTypeService.java | 98 + .../system/service/ISysLogininforService.java | 40 + .../ruoyi/system/service/ISysMenuService.java | 144 ++ .../system/service/ISysNoticeService.java | 60 + .../system/service/ISysOperLogService.java | 48 + .../ruoyi/system/service/ISysPostService.java | 99 + .../ruoyi/system/service/ISysRoleService.java | 173 ++ .../system/service/ISysUserOnlineService.java | 48 + .../ruoyi/system/service/ISysUserService.java | 206 ++ .../service/impl/SysConfigServiceImpl.java | 233 ++ .../service/impl/SysDeptServiceImpl.java | 338 +++ .../service/impl/SysDictDataServiceImpl.java | 111 + .../service/impl/SysDictTypeServiceImpl.java | 223 ++ .../impl/SysLogininforServiceImpl.java | 65 + .../service/impl/SysMenuServiceImpl.java | 543 +++++ .../service/impl/SysNoticeServiceImpl.java | 92 + .../service/impl/SysOperLogServiceImpl.java | 76 + .../service/impl/SysPostServiceImpl.java | 178 ++ .../service/impl/SysRoleServiceImpl.java | 427 ++++ .../impl/SysUserOnlineServiceImpl.java | 96 + .../service/impl/SysUserServiceImpl.java | 550 +++++ .../mapper/system/SysConfigMapper.xml | 117 + .../resources/mapper/system/SysDeptMapper.xml | 159 ++ .../mapper/system/SysDictDataMapper.xml | 124 ++ .../mapper/system/SysDictTypeMapper.xml | 105 + .../mapper/system/SysLogininforMapper.xml | 57 + .../resources/mapper/system/SysMenuMapper.xml | 206 ++ .../mapper/system/SysNoticeMapper.xml | 89 + .../mapper/system/SysOperLogMapper.xml | 87 + .../resources/mapper/system/SysPostMapper.xml | 122 ++ .../mapper/system/SysRoleDeptMapper.xml | 34 + .../resources/mapper/system/SysRoleMapper.xml | 152 ++ .../mapper/system/SysRoleMenuMapper.xml | 34 + .../resources/mapper/system/SysUserMapper.xml | 223 ++ .../mapper/system/SysUserPostMapper.xml | 34 + .../mapper/system/SysUserRoleMapper.xml | 44 + 319 files changed, 40053 insertions(+) create mode 100644 bin/clean.bat create mode 100644 bin/package.bat create mode 100644 bin/run.bat create mode 100644 doc/若依环境使用手册.docx create mode 100644 pom.xml create mode 100644 ruoyi-admin/pom.xml create mode 100644 ruoyi-admin/ruoyi-admin.iml create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/RuoYiServletInitializer.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ServerController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysLogininforController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysOperlogController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/open/SysConfigOpenController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysConfigController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictDataController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictTypeController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMenuController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysNoticeController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysPostController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/TestController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/core/config/SwaggerConfig.java create mode 100644 ruoyi-admin/src/main/resources/META-INF/spring-devtools.properties create mode 100644 ruoyi-admin/src/main/resources/application-druid.yml create mode 100644 ruoyi-admin/src/main/resources/application-third.yml create mode 100644 ruoyi-admin/src/main/resources/application.yml create mode 100644 ruoyi-admin/src/main/resources/banner.txt create mode 100644 ruoyi-admin/src/main/resources/i18n/messages.properties create mode 100644 ruoyi-admin/src/main/resources/logback.xml create mode 100644 ruoyi-admin/src/main/resources/mybatis/mybatis-config.xml create mode 100644 ruoyi-business/pom.xml create mode 100644 ruoyi-common/pom.xml create mode 100644 ruoyi-common/ruoyi-common.iml create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/annotation/Anonymous.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataScope.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSource.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excel.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excels.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/annotation/Log.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/annotation/RateLimiter.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/annotation/RepeatSubmit.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/annotation/Sensitive.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/config/serializer/SensitiveJsonSerializer.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/constant/GenConstants.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/constant/HttpStatus.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/constant/ScheduleConstants.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/controller/BaseController.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/domain/AjaxResult.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/domain/R.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeEntity.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeSelect.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDept.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictData.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictType.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysMenu.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysRole.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/RegisterBody.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/page/PageDomain.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableDataInfo.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableSupport.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/text/CharsetKit.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/text/Convert.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/core/text/StrFormatter.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessStatus.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessType.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/enums/DataSourceType.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/enums/DesensitizedType.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/enums/HttpMethod.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/enums/LimitType.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/enums/OperatorType.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/enums/UserStatus.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/exception/DemoModeException.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/exception/GlobalException.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/exception/ServiceException.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/exception/UtilException.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/exception/base/BaseException.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileException.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileNameLengthLimitExceededException.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileSizeLimitExceededException.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileUploadException.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/exception/file/InvalidExtensionException.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/exception/job/TaskException.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/exception/user/BlackListException.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaException.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaExpireException.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserException.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserNotExistsException.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordNotMatchException.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/filter/PropertyPreExcludeFilter.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatableFilter.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatedlyRequestWrapper.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/filter/XssFilter.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/filter/XssHttpServletRequestWrapper.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/Arith.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/DateUtils.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/DesensitizedUtil.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/ExceptionUtil.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/LogUtils.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/MessageUtils.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/PageUtils.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/SecurityUtils.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/StringUtils.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/Threads.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanUtils.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanValidators.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileTypeUtils.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUtils.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/file/ImageUtils.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/file/MimeTypeUtils.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/html/EscapeUtil.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/html/HTMLFilter.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpHelper.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/AddressUtils.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/IpUtils.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelHandlerAdapter.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/reflect/ReflectUtils.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Base64.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Md5Utils.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/spring/SpringUtils.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/sql/SqlUtil.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/IdUtils.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/Seq.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/UUID.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/xss/Xss.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/xss/XssValidator.java create mode 100644 ruoyi-framework/pom.xml create mode 100644 ruoyi-framework/ruoyi-framework.iml create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataScopeAspect.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataSourceAspect.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/config/ApplicationConfig.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/config/CaptchaConfig.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/config/DruidConfig.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/config/KaptchaTextCreator.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/config/ServerConfig.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/DruidProperties.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/PermitAllUrlProperties.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSource.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSourceContextHolder.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/manager/AsyncManager.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/manager/ShutdownManager.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/manager/factory/AsyncFactory.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/PermissionContextHolder.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/Server.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Cpu.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Jvm.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Mem.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Sys.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/SysFile.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPermissionService.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java create mode 100644 ruoyi-generator/pom.xml create mode 100644 ruoyi-generator/ruoyi-generator.iml create mode 100644 ruoyi-generator/src/main/java/com/ruoyi/generator/config/GenConfig.java create mode 100644 ruoyi-generator/src/main/java/com/ruoyi/generator/controller/GenController.java create mode 100644 ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTable.java create mode 100644 ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTableColumn.java create mode 100644 ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableColumnMapper.java create mode 100644 ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableMapper.java create mode 100644 ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableColumnServiceImpl.java create mode 100644 ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java create mode 100644 ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableColumnService.java create mode 100644 ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableService.java create mode 100644 ruoyi-generator/src/main/java/com/ruoyi/generator/util/GenUtils.java create mode 100644 ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityInitializer.java create mode 100644 ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java create mode 100644 ruoyi-generator/src/main/resources/generator.yml create mode 100644 ruoyi-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml create mode 100644 ruoyi-generator/src/main/resources/mapper/generator/GenTableMapper.xml create mode 100644 ruoyi-generator/src/main/resources/vm/java/controller.java.vm create mode 100644 ruoyi-generator/src/main/resources/vm/java/domain.java.vm create mode 100644 ruoyi-generator/src/main/resources/vm/java/mapper.java.vm create mode 100644 ruoyi-generator/src/main/resources/vm/java/service.java.vm create mode 100644 ruoyi-generator/src/main/resources/vm/java/serviceImpl.java.vm create mode 100644 ruoyi-generator/src/main/resources/vm/java/sub-domain.java.vm create mode 100644 ruoyi-generator/src/main/resources/vm/js/api.js.vm create mode 100644 ruoyi-generator/src/main/resources/vm/sql/sql.vm create mode 100644 ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm create mode 100644 ruoyi-generator/src/main/resources/vm/vue/index.vue.vm create mode 100644 ruoyi-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm create mode 100644 ruoyi-generator/src/main/resources/vm/vue/v3/index.vue.vm create mode 100644 ruoyi-generator/src/main/resources/vm/xml/mapper.xml.vm create mode 100644 ruoyi-quartz/pom.xml create mode 100644 ruoyi-quartz/ruoyi-quartz.iml create mode 100644 ruoyi-quartz/src/main/java/com/ruoyi/quartz/config/ScheduleConfig.java create mode 100644 ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobController.java create mode 100644 ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobLogController.java create mode 100644 ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJob.java create mode 100644 ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJobLog.java create mode 100644 ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobLogMapper.java create mode 100644 ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobMapper.java create mode 100644 ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobLogService.java create mode 100644 ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobService.java create mode 100644 ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobLogServiceImpl.java create mode 100644 ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobServiceImpl.java create mode 100644 ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java create mode 100644 ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/AbstractQuartzJob.java create mode 100644 ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/CronUtils.java create mode 100644 ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/JobInvokeUtil.java create mode 100644 ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzDisallowConcurrentExecution.java create mode 100644 ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzJobExecution.java create mode 100644 ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/ScheduleUtils.java create mode 100644 ruoyi-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml create mode 100644 ruoyi-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml create mode 100644 ruoyi-system/pom.xml create mode 100644 ruoyi-system/ruoyi-system.iml create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/SysCache.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/SysConfig.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/SysLogininfor.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/SysNotice.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOperLog.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/SysPost.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleDept.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleMenu.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserOnline.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserPost.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserRole.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/MetaVo.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/RouterVo.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysConfigMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDeptMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictTypeMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysLogininforMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysMenuMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysNoticeMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOperLogMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysPostMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleDeptMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMenuMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserPostMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserRoleMapper.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/ISysConfigService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDeptService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictDataService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictTypeService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/ISysLogininforService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/ISysMenuService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/ISysNoticeService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOperLogService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/ISysPostService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/ISysRoleService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserOnlineService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDeptServiceImpl.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictDataServiceImpl.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysLogininforServiceImpl.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysNoticeServiceImpl.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOperLogServiceImpl.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysPostServiceImpl.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserOnlineServiceImpl.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java create mode 100644 ruoyi-system/src/main/resources/mapper/system/SysConfigMapper.xml create mode 100644 ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml create mode 100644 ruoyi-system/src/main/resources/mapper/system/SysDictDataMapper.xml create mode 100644 ruoyi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml create mode 100644 ruoyi-system/src/main/resources/mapper/system/SysLogininforMapper.xml create mode 100644 ruoyi-system/src/main/resources/mapper/system/SysMenuMapper.xml create mode 100644 ruoyi-system/src/main/resources/mapper/system/SysNoticeMapper.xml create mode 100644 ruoyi-system/src/main/resources/mapper/system/SysOperLogMapper.xml create mode 100644 ruoyi-system/src/main/resources/mapper/system/SysPostMapper.xml create mode 100644 ruoyi-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml create mode 100644 ruoyi-system/src/main/resources/mapper/system/SysRoleMapper.xml create mode 100644 ruoyi-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml create mode 100644 ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml create mode 100644 ruoyi-system/src/main/resources/mapper/system/SysUserPostMapper.xml create mode 100644 ruoyi-system/src/main/resources/mapper/system/SysUserRoleMapper.xml diff --git a/bin/clean.bat b/bin/clean.bat new file mode 100644 index 0000000..24c0974 --- /dev/null +++ b/bin/clean.bat @@ -0,0 +1,12 @@ +@echo off +echo. +echo [Ϣ] target· +echo. + +%~d0 +cd %~dp0 + +cd .. +call mvn clean + +pause \ No newline at end of file diff --git a/bin/package.bat b/bin/package.bat new file mode 100644 index 0000000..c693ec0 --- /dev/null +++ b/bin/package.bat @@ -0,0 +1,12 @@ +@echo off +echo. +echo [Ϣ] Weḅwar/jarļ +echo. + +%~d0 +cd %~dp0 + +cd .. +call mvn clean package -Dmaven.test.skip=true + +pause \ No newline at end of file diff --git a/bin/run.bat b/bin/run.bat new file mode 100644 index 0000000..41efbd0 --- /dev/null +++ b/bin/run.bat @@ -0,0 +1,14 @@ +@echo off +echo. +echo [Ϣ] ʹJarWeb̡ +echo. + +cd %~dp0 +cd ../ruoyi-admin/target + +set JAVA_OPTS=-Xms256m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m + +java -jar %JAVA_OPTS% ruoyi-admin.jar + +cd bin +pause \ No newline at end of file diff --git a/doc/若依环境使用手册.docx b/doc/若依环境使用手册.docx new file mode 100644 index 0000000000000000000000000000000000000000..19d2fc3da32eee76d8539c2e72b76f7ef4723944 GIT binary patch literal 428152 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

?d4lyDNoK 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 d0l*2_g;wn>@b<3 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Ğj?1V6F=S(3 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�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

XvvyD~nm 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 DJjES0 literal 0 HcmV?d00001 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..0deca9e --- /dev/null +++ b/pom.xml @@ -0,0 +1,281 @@ + + + 4.0.0 + + com.ruoyi + ruoyi + 3.9.0 + + ruoyi + 管理系统 + + + 3.9.0 + UTF-8 + UTF-8 + 1.8 + 3.1.1 + 2.5.15 + 1.2.23 + 1.21 + 3.0.0 + 2.3.3 + 1.4.7 + 2.0.57 + 6.8.2 + 2.19.0 + 4.1.2 + 2.3 + 0.9.1 + + 9.0.106 + 1.2.13 + 5.7.12 + 5.3.39 + + + + + + + + + org.springframework + spring-framework-bom + ${spring-framework.version} + pom + import + + + + + org.springframework.security + spring-security-bom + ${spring-security.version} + pom + import + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + ch.qos.logback + logback-core + ${logback.version} + + + + ch.qos.logback + logback-classic + ${logback.version} + + + + + org.apache.tomcat.embed + tomcat-embed-core + ${tomcat.version} + + + + org.apache.tomcat.embed + tomcat-embed-el + ${tomcat.version} + + + + org.apache.tomcat.embed + tomcat-embed-websocket + ${tomcat.version} + + + + + com.alibaba + druid-spring-boot-starter + ${druid.version} + + + + + eu.bitwalker + UserAgentUtils + ${bitwalker.version} + + + + + com.github.pagehelper + pagehelper-spring-boot-starter + ${pagehelper.boot.version} + + + + + com.github.oshi + oshi-core + ${oshi.version} + + + + + io.springfox + springfox-boot-starter + ${swagger.version} + + + io.swagger + swagger-models + + + + + + + commons-io + commons-io + ${commons.io.version} + + + + + org.apache.poi + poi-ooxml + ${poi.version} + + + + + org.apache.velocity + velocity-engine-core + ${velocity.version} + + + + + com.alibaba.fastjson2 + fastjson2 + ${fastjson.version} + + + + + io.jsonwebtoken + jjwt + ${jwt.version} + + + + + pro.fessional + kaptcha + ${kaptcha.version} + + + + + com.ruoyi + ruoyi-quartz + ${ruoyi.version} + + + + + com.ruoyi + ruoyi-generator + ${ruoyi.version} + + + + + com.ruoyi + ruoyi-framework + ${ruoyi.version} + + + + + com.ruoyi + ruoyi-system + ${ruoyi.version} + + + + + com.ruoyi + ruoyi-common + ${ruoyi.version} + + + + + com.ruoyi + ruoyi-business + ${ruoyi.version} + + + + + + + ruoyi-admin + ruoyi-framework + ruoyi-system + ruoyi-quartz + ruoyi-generator + ruoyi-common + ruoyi-business + + pom + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + ${java.version} + ${java.version} + ${project.build.sourceEncoding} + + + + + + + + public + aliyun nexus + https://maven.aliyun.com/repository/public + + true + + + + + + + public + aliyun nexus + https://maven.aliyun.com/repository/public + + true + + + false + + + + + diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml new file mode 100644 index 0000000..c05f707 --- /dev/null +++ b/ruoyi-admin/pom.xml @@ -0,0 +1,96 @@ + + + + ruoyi + com.ruoyi + 3.9.0 + + 4.0.0 + jar + ruoyi-admin + + + web服务入口 + + + + + + + org.springframework.boot + spring-boot-devtools + true + + + + + io.springfox + springfox-boot-starter + + + + + io.swagger + swagger-models + 1.6.2 + + + + + mysql + mysql-connector-java + + + + + com.ruoyi + ruoyi-framework + + + + + com.ruoyi + ruoyi-quartz + + + + + com.ruoyi + ruoyi-generator + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + 2.5.15 + + true + + + + + repackage + + + + + + org.apache.maven.plugins + maven-war-plugin + 3.1.0 + + false + ${project.artifactId} + + + + ${project.artifactId} + + + \ No newline at end of file diff --git a/ruoyi-admin/ruoyi-admin.iml b/ruoyi-admin/ruoyi-admin.iml new file mode 100644 index 0000000..2d51d4d --- /dev/null +++ b/ruoyi-admin/ruoyi-admin.iml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java b/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java new file mode 100644 index 0000000..32eb6f1 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java @@ -0,0 +1,30 @@ +package com.ruoyi; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; + +/** + * 启动程序 + * + * @author ruoyi + */ +@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }) +public class RuoYiApplication +{ + public static void main(String[] args) + { + // System.setProperty("spring.devtools.restart.enabled", "false"); + SpringApplication.run(RuoYiApplication.class, args); + System.out.println("(♥◠‿◠)ノ゙ 若依启动成功 ლ(´ڡ`ლ)゙ \n" + + " .-------. ____ __ \n" + + " | _ _ \\ \\ \\ / / \n" + + " | ( ' ) | \\ _. / ' \n" + + " |(_ o _) / _( )_ .' \n" + + " | (_,_).' __ ___(_ o _)' \n" + + " | |\\ \\ | || |(_,_)' \n" + + " | | \\ `' /| `-' / \n" + + " | | \\ / \\ / \n" + + " ''-' `'-' `-..-' "); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/RuoYiServletInitializer.java b/ruoyi-admin/src/main/java/com/ruoyi/RuoYiServletInitializer.java new file mode 100644 index 0000000..6de67dc --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/RuoYiServletInitializer.java @@ -0,0 +1,18 @@ +package com.ruoyi; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +/** + * web容器中进行部署 + * + * @author ruoyi + */ +public class RuoYiServletInitializer extends SpringBootServletInitializer +{ + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) + { + return application.sources(RuoYiApplication.class); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java new file mode 100644 index 0000000..d2d6e8c --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java @@ -0,0 +1,94 @@ +package com.ruoyi.web.controller.common; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import javax.annotation.Resource; +import javax.imageio.ImageIO; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.FastByteArrayOutputStream; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import com.google.code.kaptcha.Producer; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.sign.Base64; +import com.ruoyi.common.utils.uuid.IdUtils; +import com.ruoyi.system.service.ISysConfigService; + +/** + * 验证码操作处理 + * + * @author ruoyi + */ +@RestController +public class CaptchaController +{ + @Resource(name = "captchaProducer") + private Producer captchaProducer; + + @Resource(name = "captchaProducerMath") + private Producer captchaProducerMath; + + @Autowired + private RedisCache redisCache; + + @Autowired + private ISysConfigService configService; + /** + * 生成验证码 + */ + @GetMapping("/captchaImage") + public AjaxResult getCode(HttpServletResponse response) throws IOException + { + AjaxResult ajax = AjaxResult.success(); + boolean captchaEnabled = configService.selectCaptchaEnabled(); + ajax.put("captchaEnabled", captchaEnabled); + if (!captchaEnabled) + { + return ajax; + } + + // 保存验证码信息 + String uuid = IdUtils.simpleUUID(); + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid; + + String capStr = null, code = null; + BufferedImage image = null; + + // 生成验证码 + String captchaType = RuoYiConfig.getCaptchaType(); + if ("math".equals(captchaType)) + { + String capText = captchaProducerMath.createText(); + capStr = capText.substring(0, capText.lastIndexOf("@")); + code = capText.substring(capText.lastIndexOf("@") + 1); + image = captchaProducerMath.createImage(capStr); + } + else if ("char".equals(captchaType)) + { + capStr = code = captchaProducer.createText(); + image = captchaProducer.createImage(capStr); + } + + redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES); + // 转换流信息写出 + FastByteArrayOutputStream os = new FastByteArrayOutputStream(); + try + { + ImageIO.write(image, "jpg", os); + } + catch (IOException e) + { + return AjaxResult.error(e.getMessage()); + } + + ajax.put("uuid", uuid); + ajax.put("img", Base64.encode(os.toByteArray())); + return ajax; + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java new file mode 100644 index 0000000..f75721e --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java @@ -0,0 +1,162 @@ +package com.ruoyi.web.controller.common; + +import java.util.ArrayList; +import java.util.List; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.file.FileUploadUtils; +import com.ruoyi.common.utils.file.FileUtils; +import com.ruoyi.framework.config.ServerConfig; + +/** + * 通用请求处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/common") +public class CommonController +{ + private static final Logger log = LoggerFactory.getLogger(CommonController.class); + + @Autowired + private ServerConfig serverConfig; + + private static final String FILE_DELIMETER = ","; + + /** + * 通用下载请求 + * + * @param fileName 文件名称 + * @param delete 是否删除 + */ + @GetMapping("/download") + public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request) + { + try + { + if (!FileUtils.checkAllowDownload(fileName)) + { + throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName)); + } + String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1); + String filePath = RuoYiConfig.getDownloadPath() + fileName; + + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + FileUtils.setAttachmentResponseHeader(response, realFileName); + FileUtils.writeBytes(filePath, response.getOutputStream()); + if (delete) + { + FileUtils.deleteFile(filePath); + } + } + catch (Exception e) + { + log.error("下载文件失败", e); + } + } + + /** + * 通用上传请求(单个) + */ + @PostMapping("/upload") + public AjaxResult uploadFile(MultipartFile file) throws Exception + { + try + { + // 上传文件路径 + String filePath = RuoYiConfig.getUploadPath(); + // 上传并返回新文件名称 + String fileName = FileUploadUtils.upload(filePath, file); + String url = serverConfig.getUrl() + fileName; + AjaxResult ajax = AjaxResult.success(); + ajax.put("url", url); + ajax.put("fileName", fileName); + ajax.put("newFileName", FileUtils.getName(fileName)); + ajax.put("originalFilename", file.getOriginalFilename()); + return ajax; + } + catch (Exception e) + { + return AjaxResult.error(e.getMessage()); + } + } + + /** + * 通用上传请求(多个) + */ + @PostMapping("/uploads") + public AjaxResult uploadFiles(List files) throws Exception + { + try + { + // 上传文件路径 + String filePath = RuoYiConfig.getUploadPath(); + List urls = new ArrayList(); + List fileNames = new ArrayList(); + List newFileNames = new ArrayList(); + List originalFilenames = new ArrayList(); + for (MultipartFile file : files) + { + // 上传并返回新文件名称 + String fileName = FileUploadUtils.upload(filePath, file); + String url = serverConfig.getUrl() + fileName; + urls.add(url); + fileNames.add(fileName); + newFileNames.add(FileUtils.getName(fileName)); + originalFilenames.add(file.getOriginalFilename()); + } + AjaxResult ajax = AjaxResult.success(); + ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER)); + ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER)); + ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER)); + ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER)); + return ajax; + } + catch (Exception e) + { + return AjaxResult.error(e.getMessage()); + } + } + + /** + * 本地资源通用下载 + */ + @GetMapping("/download/resource") + public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response) + throws Exception + { + try + { + if (!FileUtils.checkAllowDownload(resource)) + { + throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource)); + } + // 本地资源路径 + String localPath = RuoYiConfig.getProfile(); + // 数据库资源地址 + String downloadPath = localPath + FileUtils.stripPrefix(resource); + // 下载名称 + String downloadName = StringUtils.substringAfterLast(downloadPath, "/"); + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + FileUtils.setAttachmentResponseHeader(response, downloadName); + FileUtils.writeBytes(downloadPath, response.getOutputStream()); + } + catch (Exception e) + { + log.error("下载文件失败", e); + } + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java new file mode 100644 index 0000000..c8c49c9 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java @@ -0,0 +1,121 @@ +package com.ruoyi.web.controller.monitor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.TreeSet; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.SysCache; + +/** + * 缓存监控 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/cache") +public class CacheController +{ + @Autowired + private RedisTemplate redisTemplate; + + private final static List caches = new ArrayList(); + { + caches.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息")); + caches.add(new SysCache(CacheConstants.SYS_CONFIG_KEY, "配置信息")); + caches.add(new SysCache(CacheConstants.SYS_DICT_KEY, "数据字典")); + caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码")); + caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交")); + caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理")); + caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数")); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @GetMapping() + public AjaxResult getInfo() throws Exception + { + Properties info = (Properties) redisTemplate.execute((RedisCallback) connection -> connection.info()); + Properties commandStats = (Properties) redisTemplate.execute((RedisCallback) connection -> connection.info("commandstats")); + Object dbSize = redisTemplate.execute((RedisCallback) connection -> connection.dbSize()); + + Map result = new HashMap<>(3); + result.put("info", info); + result.put("dbSize", dbSize); + + List> pieList = new ArrayList<>(); + commandStats.stringPropertyNames().forEach(key -> { + Map data = new HashMap<>(2); + String property = commandStats.getProperty(key); + data.put("name", StringUtils.removeStart(key, "cmdstat_")); + data.put("value", StringUtils.substringBetween(property, "calls=", ",usec")); + pieList.add(data); + }); + result.put("commandStats", pieList); + return AjaxResult.success(result); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @GetMapping("/getNames") + public AjaxResult cache() + { + return AjaxResult.success(caches); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @GetMapping("/getKeys/{cacheName}") + public AjaxResult getCacheKeys(@PathVariable String cacheName) + { + Set cacheKeys = redisTemplate.keys(cacheName + "*"); + return AjaxResult.success(new TreeSet<>(cacheKeys)); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @GetMapping("/getValue/{cacheName}/{cacheKey}") + public AjaxResult getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey) + { + String cacheValue = redisTemplate.opsForValue().get(cacheKey); + SysCache sysCache = new SysCache(cacheName, cacheKey, cacheValue); + return AjaxResult.success(sysCache); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @DeleteMapping("/clearCacheName/{cacheName}") + public AjaxResult clearCacheName(@PathVariable String cacheName) + { + Collection cacheKeys = redisTemplate.keys(cacheName + "*"); + redisTemplate.delete(cacheKeys); + return AjaxResult.success(); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @DeleteMapping("/clearCacheKey/{cacheKey}") + public AjaxResult clearCacheKey(@PathVariable String cacheKey) + { + redisTemplate.delete(cacheKey); + return AjaxResult.success(); + } + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @DeleteMapping("/clearCacheAll") + public AjaxResult clearCacheAll() + { + Collection cacheKeys = redisTemplate.keys("*"); + redisTemplate.delete(cacheKeys); + return AjaxResult.success(); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ServerController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ServerController.java new file mode 100644 index 0000000..cc805ad --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ServerController.java @@ -0,0 +1,27 @@ +package com.ruoyi.web.controller.monitor; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.framework.web.domain.Server; + +/** + * 服务器监控 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/server") +public class ServerController +{ + @PreAuthorize("@ss.hasPermi('monitor:server:list')") + @GetMapping() + public AjaxResult getInfo() throws Exception + { + Server server = new Server(); + server.copyTo(); + return AjaxResult.success(server); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysLogininforController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysLogininforController.java new file mode 100644 index 0000000..e0175f4 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysLogininforController.java @@ -0,0 +1,82 @@ +package com.ruoyi.web.controller.monitor; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.framework.web.service.SysPasswordService; +import com.ruoyi.system.domain.SysLogininfor; +import com.ruoyi.system.service.ISysLogininforService; + +/** + * 系统访问记录 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/logininfor") +public class SysLogininforController extends BaseController +{ + @Autowired + private ISysLogininforService logininforService; + + @Autowired + private SysPasswordService passwordService; + + @PreAuthorize("@ss.hasPermi('monitor:logininfor:list')") + @GetMapping("/list") + public TableDataInfo list(SysLogininfor logininfor) + { + startPage(); + List list = logininforService.selectLogininforList(logininfor); + return getDataTable(list); + } + + @Log(title = "登录日志", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('monitor:logininfor:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysLogininfor logininfor) + { + List list = logininforService.selectLogininforList(logininfor); + ExcelUtil util = new ExcelUtil(SysLogininfor.class); + util.exportExcel(response, list, "登录日志"); + } + + @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')") + @Log(title = "登录日志", businessType = BusinessType.DELETE) + @DeleteMapping("/{infoIds}") + public AjaxResult remove(@PathVariable Long[] infoIds) + { + return toAjax(logininforService.deleteLogininforByIds(infoIds)); + } + + @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')") + @Log(title = "登录日志", businessType = BusinessType.CLEAN) + @DeleteMapping("/clean") + public AjaxResult clean() + { + logininforService.cleanLogininfor(); + return success(); + } + + @PreAuthorize("@ss.hasPermi('monitor:logininfor:unlock')") + @Log(title = "账户解锁", businessType = BusinessType.OTHER) + @GetMapping("/unlock/{userName}") + public AjaxResult unlock(@PathVariable("userName") String userName) + { + passwordService.clearLoginRecordCache(userName); + return success(); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysOperlogController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysOperlogController.java new file mode 100644 index 0000000..6ca78cf --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysOperlogController.java @@ -0,0 +1,69 @@ +package com.ruoyi.web.controller.monitor; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.system.domain.SysOperLog; +import com.ruoyi.system.service.ISysOperLogService; + +/** + * 操作日志记录 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/operlog") +public class SysOperlogController extends BaseController +{ + @Autowired + private ISysOperLogService operLogService; + + @PreAuthorize("@ss.hasPermi('monitor:operlog:list')") + @GetMapping("/list") + public TableDataInfo list(SysOperLog operLog) + { + startPage(); + List list = operLogService.selectOperLogList(operLog); + return getDataTable(list); + } + + @Log(title = "操作日志", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('monitor:operlog:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysOperLog operLog) + { + List list = operLogService.selectOperLogList(operLog); + ExcelUtil util = new ExcelUtil(SysOperLog.class); + util.exportExcel(response, list, "操作日志"); + } + + @Log(title = "操作日志", businessType = BusinessType.DELETE) + @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')") + @DeleteMapping("/{operIds}") + public AjaxResult remove(@PathVariable Long[] operIds) + { + return toAjax(operLogService.deleteOperLogByIds(operIds)); + } + + @Log(title = "操作日志", businessType = BusinessType.CLEAN) + @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')") + @DeleteMapping("/clean") + public AjaxResult clean() + { + operLogService.cleanOperLog(); + return success(); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java new file mode 100644 index 0000000..a442863 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java @@ -0,0 +1,83 @@ +package com.ruoyi.web.controller.monitor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.SysUserOnline; +import com.ruoyi.system.service.ISysUserOnlineService; + +/** + * 在线用户监控 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/online") +public class SysUserOnlineController extends BaseController +{ + @Autowired + private ISysUserOnlineService userOnlineService; + + @Autowired + private RedisCache redisCache; + + @PreAuthorize("@ss.hasPermi('monitor:online:list')") + @GetMapping("/list") + public TableDataInfo list(String ipaddr, String userName) + { + Collection keys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*"); + List userOnlineList = new ArrayList(); + for (String key : keys) + { + LoginUser user = redisCache.getCacheObject(key); + if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName)) + { + userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user)); + } + else if (StringUtils.isNotEmpty(ipaddr)) + { + userOnlineList.add(userOnlineService.selectOnlineByIpaddr(ipaddr, user)); + } + else if (StringUtils.isNotEmpty(userName) && StringUtils.isNotNull(user.getUser())) + { + userOnlineList.add(userOnlineService.selectOnlineByUserName(userName, user)); + } + else + { + userOnlineList.add(userOnlineService.loginUserToUserOnline(user)); + } + } + Collections.reverse(userOnlineList); + userOnlineList.removeAll(Collections.singleton(null)); + return getDataTable(userOnlineList); + } + + /** + * 强退用户 + */ + @PreAuthorize("@ss.hasPermi('monitor:online:forceLogout')") + @Log(title = "在线用户", businessType = BusinessType.FORCE) + @DeleteMapping("/{tokenId}") + public AjaxResult forceLogout(@PathVariable String tokenId) + { + redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + tokenId); + return success(); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/open/SysConfigOpenController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/open/SysConfigOpenController.java new file mode 100644 index 0000000..ead0e5b --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/open/SysConfigOpenController.java @@ -0,0 +1,52 @@ +package com.ruoyi.web.controller.open; + +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.system.domain.SysConfig; +import com.ruoyi.system.service.ISysConfigService; +import io.swagger.annotations.Api; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 通用参数放行 + * @author: zzl + * @date: Created in 2023-06-05 11:47 + * @version: 1.0 + * @modified By: + */ + +@Api(tags = "通用参数") +@RestController +@RequestMapping("/open/typz") +public class SysConfigOpenController extends BaseController { + + @Autowired + private ISysConfigService configService; + + /** + * 参数列表 + */ + @GetMapping("/list") + public TableDataInfo list(SysConfig config) + { + startPage(); + List list = configService.selectConfigList(config); + return getDataTable(list); + } + + /** + * 根据参数键名查询参数值 + */ + @GetMapping(value = "/configKey/{configKey}") + public AjaxResult getConfigKey(@PathVariable String configKey) + { + return AjaxResult.success(configService.selectConfigByKey(configKey)); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysConfigController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysConfigController.java new file mode 100644 index 0000000..ab4653d --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysConfigController.java @@ -0,0 +1,133 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.system.domain.SysConfig; +import com.ruoyi.system.service.ISysConfigService; + +/** + * 参数配置 信息操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/config") +public class SysConfigController extends BaseController +{ + @Autowired + private ISysConfigService configService; + + /** + * 获取参数配置列表 + */ + @PreAuthorize("@ss.hasPermi('system:config:list')") + @GetMapping("/list") + public TableDataInfo list(SysConfig config) + { + startPage(); + List list = configService.selectConfigList(config); + return getDataTable(list); + } + + @Log(title = "参数管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:config:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysConfig config) + { + List list = configService.selectConfigList(config); + ExcelUtil util = new ExcelUtil(SysConfig.class); + util.exportExcel(response, list, "参数数据"); + } + + /** + * 根据参数编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:config:query')") + @GetMapping(value = "/{configId}") + public AjaxResult getInfo(@PathVariable Long configId) + { + return success(configService.selectConfigById(configId)); + } + + /** + * 根据参数键名查询参数值 + */ + @GetMapping(value = "/configKey/{configKey}") + public AjaxResult getConfigKey(@PathVariable String configKey) + { + return success(configService.selectConfigByKey(configKey)); + } + + /** + * 新增参数配置 + */ + @PreAuthorize("@ss.hasPermi('system:config:add')") + @Log(title = "参数管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysConfig config) + { + if (!configService.checkConfigKeyUnique(config)) + { + return error("新增参数'" + config.getConfigName() + "'失败,参数键名已存在"); + } + config.setCreateBy(getUsername()); + return toAjax(configService.insertConfig(config)); + } + + /** + * 修改参数配置 + */ + @PreAuthorize("@ss.hasPermi('system:config:edit')") + @Log(title = "参数管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysConfig config) + { + if (!configService.checkConfigKeyUnique(config)) + { + return error("修改参数'" + config.getConfigName() + "'失败,参数键名已存在"); + } + config.setUpdateBy(getUsername()); + return toAjax(configService.updateConfig(config)); + } + + /** + * 删除参数配置 + */ + @PreAuthorize("@ss.hasPermi('system:config:remove')") + @Log(title = "参数管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{configIds}") + public AjaxResult remove(@PathVariable Long[] configIds) + { + configService.deleteConfigByIds(configIds); + return success(); + } + + /** + * 刷新参数缓存 + */ + @PreAuthorize("@ss.hasPermi('system:config:remove')") + @Log(title = "参数管理", businessType = BusinessType.CLEAN) + @DeleteMapping("/refreshCache") + public AjaxResult refreshCache() + { + configService.resetConfigCache(); + return success(); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java new file mode 100644 index 0000000..59e7588 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java @@ -0,0 +1,132 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysDept; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.service.ISysDeptService; + +/** + * 部门信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/dept") +public class SysDeptController extends BaseController +{ + @Autowired + private ISysDeptService deptService; + + /** + * 获取部门列表 + */ + @PreAuthorize("@ss.hasPermi('system:dept:list')") + @GetMapping("/list") + public AjaxResult list(SysDept dept) + { + List depts = deptService.selectDeptList(dept); + return success(depts); + } + + /** + * 查询部门列表(排除节点) + */ + @PreAuthorize("@ss.hasPermi('system:dept:list')") + @GetMapping("/list/exclude/{deptId}") + public AjaxResult excludeChild(@PathVariable(value = "deptId", required = false) Long deptId) + { + List depts = deptService.selectDeptList(new SysDept()); + depts.removeIf(d -> d.getDeptId().intValue() == deptId || ArrayUtils.contains(StringUtils.split(d.getAncestors(), ","), deptId + "")); + return success(depts); + } + + /** + * 根据部门编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:dept:query')") + @GetMapping(value = "/{deptId}") + public AjaxResult getInfo(@PathVariable Long deptId) + { + deptService.checkDeptDataScope(deptId); + return success(deptService.selectDeptById(deptId)); + } + + /** + * 新增部门 + */ + @PreAuthorize("@ss.hasPermi('system:dept:add')") + @Log(title = "部门管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysDept dept) + { + if (!deptService.checkDeptNameUnique(dept)) + { + return error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在"); + } + dept.setCreateBy(getUsername()); + return toAjax(deptService.insertDept(dept)); + } + + /** + * 修改部门 + */ + @PreAuthorize("@ss.hasPermi('system:dept:edit')") + @Log(title = "部门管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysDept dept) + { + Long deptId = dept.getDeptId(); + deptService.checkDeptDataScope(deptId); + if (!deptService.checkDeptNameUnique(dept)) + { + return error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在"); + } + else if (dept.getParentId().equals(deptId)) + { + return error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己"); + } + else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()) && deptService.selectNormalChildrenDeptById(deptId) > 0) + { + return error("该部门包含未停用的子部门!"); + } + dept.setUpdateBy(getUsername()); + return toAjax(deptService.updateDept(dept)); + } + + /** + * 删除部门 + */ + @PreAuthorize("@ss.hasPermi('system:dept:remove')") + @Log(title = "部门管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{deptId}") + public AjaxResult remove(@PathVariable Long deptId) + { + if (deptService.hasChildByDeptId(deptId)) + { + return warn("存在下级部门,不允许删除"); + } + if (deptService.checkDeptExistUser(deptId)) + { + return warn("部门存在用户,不允许删除"); + } + deptService.checkDeptDataScope(deptId); + return toAjax(deptService.deleteDeptById(deptId)); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictDataController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictDataController.java new file mode 100644 index 0000000..59becaf --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictDataController.java @@ -0,0 +1,121 @@ +package com.ruoyi.web.controller.system; + +import java.util.ArrayList; +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.system.service.ISysDictDataService; +import com.ruoyi.system.service.ISysDictTypeService; + +/** + * 数据字典信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/dict/data") +public class SysDictDataController extends BaseController +{ + @Autowired + private ISysDictDataService dictDataService; + + @Autowired + private ISysDictTypeService dictTypeService; + + @PreAuthorize("@ss.hasPermi('system:dict:list')") + @GetMapping("/list") + public TableDataInfo list(SysDictData dictData) + { + startPage(); + List list = dictDataService.selectDictDataList(dictData); + return getDataTable(list); + } + + @Log(title = "字典数据", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:dict:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysDictData dictData) + { + List list = dictDataService.selectDictDataList(dictData); + ExcelUtil util = new ExcelUtil(SysDictData.class); + util.exportExcel(response, list, "字典数据"); + } + + /** + * 查询字典数据详细 + */ + @PreAuthorize("@ss.hasPermi('system:dict:query')") + @GetMapping(value = "/{dictCode}") + public AjaxResult getInfo(@PathVariable Long dictCode) + { + return success(dictDataService.selectDictDataById(dictCode)); + } + + /** + * 根据字典类型查询字典数据信息 + */ + @GetMapping(value = "/type/{dictType}") + public AjaxResult dictType(@PathVariable String dictType) + { + List data = dictTypeService.selectDictDataByType(dictType); + if (StringUtils.isNull(data)) + { + data = new ArrayList(); + } + return success(data); + } + + /** + * 新增字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:add')") + @Log(title = "字典数据", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysDictData dict) + { + dict.setCreateBy(getUsername()); + return toAjax(dictDataService.insertDictData(dict)); + } + + /** + * 修改保存字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:edit')") + @Log(title = "字典数据", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysDictData dict) + { + dict.setUpdateBy(getUsername()); + return toAjax(dictDataService.updateDictData(dict)); + } + + /** + * 删除字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:remove')") + @Log(title = "字典类型", businessType = BusinessType.DELETE) + @DeleteMapping("/{dictCodes}") + public AjaxResult remove(@PathVariable Long[] dictCodes) + { + dictDataService.deleteDictDataByIds(dictCodes); + return success(); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictTypeController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictTypeController.java new file mode 100644 index 0000000..c53867c --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictTypeController.java @@ -0,0 +1,131 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysDictType; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.system.service.ISysDictTypeService; + +/** + * 数据字典信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/dict/type") +public class SysDictTypeController extends BaseController +{ + @Autowired + private ISysDictTypeService dictTypeService; + + @PreAuthorize("@ss.hasPermi('system:dict:list')") + @GetMapping("/list") + public TableDataInfo list(SysDictType dictType) + { + startPage(); + List list = dictTypeService.selectDictTypeList(dictType); + return getDataTable(list); + } + + @Log(title = "字典类型", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:dict:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysDictType dictType) + { + List list = dictTypeService.selectDictTypeList(dictType); + ExcelUtil util = new ExcelUtil(SysDictType.class); + util.exportExcel(response, list, "字典类型"); + } + + /** + * 查询字典类型详细 + */ + @PreAuthorize("@ss.hasPermi('system:dict:query')") + @GetMapping(value = "/{dictId}") + public AjaxResult getInfo(@PathVariable Long dictId) + { + return success(dictTypeService.selectDictTypeById(dictId)); + } + + /** + * 新增字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:add')") + @Log(title = "字典类型", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysDictType dict) + { + if (!dictTypeService.checkDictTypeUnique(dict)) + { + return error("新增字典'" + dict.getDictName() + "'失败,字典类型已存在"); + } + dict.setCreateBy(getUsername()); + return toAjax(dictTypeService.insertDictType(dict)); + } + + /** + * 修改字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:edit')") + @Log(title = "字典类型", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysDictType dict) + { + if (!dictTypeService.checkDictTypeUnique(dict)) + { + return error("修改字典'" + dict.getDictName() + "'失败,字典类型已存在"); + } + dict.setUpdateBy(getUsername()); + return toAjax(dictTypeService.updateDictType(dict)); + } + + /** + * 删除字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:remove')") + @Log(title = "字典类型", businessType = BusinessType.DELETE) + @DeleteMapping("/{dictIds}") + public AjaxResult remove(@PathVariable Long[] dictIds) + { + dictTypeService.deleteDictTypeByIds(dictIds); + return success(); + } + + /** + * 刷新字典缓存 + */ + @PreAuthorize("@ss.hasPermi('system:dict:remove')") + @Log(title = "字典类型", businessType = BusinessType.CLEAN) + @DeleteMapping("/refreshCache") + public AjaxResult refreshCache() + { + dictTypeService.resetDictCache(); + return success(); + } + + /** + * 获取字典选择框列表 + */ + @GetMapping("/optionselect") + public AjaxResult optionselect() + { + List dictTypes = dictTypeService.selectDictTypeAll(); + return success(dictTypes); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java new file mode 100644 index 0000000..13007eb --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java @@ -0,0 +1,29 @@ +package com.ruoyi.web.controller.system; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.utils.StringUtils; + +/** + * 首页 + * + * @author ruoyi + */ +@RestController +public class SysIndexController +{ + /** 系统基础配置 */ + @Autowired + private RuoYiConfig ruoyiConfig; + + /** + * 访问首页,提示语 + */ + @RequestMapping("/") + public String index() + { + return StringUtils.format("欢迎使用{}后台管理框架,当前版本:v{},请通过前端地址访问。", ruoyiConfig.getName(), ruoyiConfig.getVersion()); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java new file mode 100644 index 0000000..8be0f0e --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java @@ -0,0 +1,131 @@ +package com.ruoyi.web.controller.system; + +import java.util.Date; +import java.util.List; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysMenu; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginBody; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.web.service.SysLoginService; +import com.ruoyi.framework.web.service.SysPermissionService; +import com.ruoyi.framework.web.service.TokenService; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.system.service.ISysMenuService; + +/** + * 登录验证 + * + * @author ruoyi + */ +@RestController +public class SysLoginController +{ + @Autowired + private SysLoginService loginService; + + @Autowired + private ISysMenuService menuService; + + @Autowired + private SysPermissionService permissionService; + + @Autowired + private TokenService tokenService; + + @Autowired + private ISysConfigService configService; + + /** + * 登录方法 + * + * @param loginBody 登录信息 + * @return 结果 + */ + @PostMapping("/login") + public AjaxResult login(@RequestBody LoginBody loginBody) + { + AjaxResult ajax = AjaxResult.success(); + // 生成令牌 + String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(), + loginBody.getUuid()); + ajax.put(Constants.TOKEN, token); + return ajax; + } + + /** + * 获取用户信息 + * + * @return 用户信息 + */ + @GetMapping("getInfo") + public AjaxResult getInfo() + { + LoginUser loginUser = SecurityUtils.getLoginUser(); + SysUser user = loginUser.getUser(); + // 角色集合 + Set roles = permissionService.getRolePermission(user); + // 权限集合 + Set permissions = permissionService.getMenuPermission(user); + if (!loginUser.getPermissions().equals(permissions)) + { + loginUser.setPermissions(permissions); + tokenService.refreshToken(loginUser); + } + AjaxResult ajax = AjaxResult.success(); + ajax.put("user", user); + ajax.put("roles", roles); + ajax.put("permissions", permissions); + ajax.put("isDefaultModifyPwd", initPasswordIsModify(user.getPwdUpdateDate())); + ajax.put("isPasswordExpired", passwordIsExpiration(user.getPwdUpdateDate())); + return ajax; + } + + /** + * 获取路由信息 + * + * @return 路由信息 + */ + @GetMapping("getRouters") + public AjaxResult getRouters() + { + Long userId = SecurityUtils.getUserId(); + List menus = menuService.selectMenuTreeByUserId(userId); + return AjaxResult.success(menuService.buildMenus(menus)); + } + + // 检查初始密码是否提醒修改 + public boolean initPasswordIsModify(Date pwdUpdateDate) + { + Integer initPasswordModify = Convert.toInt(configService.selectConfigByKey("sys.account.initPasswordModify")); + return initPasswordModify != null && initPasswordModify == 1 && pwdUpdateDate == null; + } + + // 检查密码是否过期 + public boolean passwordIsExpiration(Date pwdUpdateDate) + { + Integer passwordValidateDays = Convert.toInt(configService.selectConfigByKey("sys.account.passwordValidateDays")); + if (passwordValidateDays != null && passwordValidateDays > 0) + { + if (StringUtils.isNull(pwdUpdateDate)) + { + // 如果从未修改过初始密码,直接提醒过期 + return true; + } + Date nowDate = DateUtils.getNowDate(); + return DateUtils.differentDaysByMillisecond(nowDate, pwdUpdateDate) > passwordValidateDays; + } + return false; + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMenuController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMenuController.java new file mode 100644 index 0000000..03b6b65 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMenuController.java @@ -0,0 +1,142 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysMenu; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.service.ISysMenuService; + +/** + * 菜单信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/menu") +public class SysMenuController extends BaseController +{ + @Autowired + private ISysMenuService menuService; + + /** + * 获取菜单列表 + */ + @PreAuthorize("@ss.hasPermi('system:menu:list')") + @GetMapping("/list") + public AjaxResult list(SysMenu menu) + { + List menus = menuService.selectMenuList(menu, getUserId()); + return success(menus); + } + + /** + * 根据菜单编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:menu:query')") + @GetMapping(value = "/{menuId}") + public AjaxResult getInfo(@PathVariable Long menuId) + { + return success(menuService.selectMenuById(menuId)); + } + + /** + * 获取菜单下拉树列表 + */ + @GetMapping("/treeselect") + public AjaxResult treeselect(SysMenu menu) + { + List menus = menuService.selectMenuList(menu, getUserId()); + return success(menuService.buildMenuTreeSelect(menus)); + } + + /** + * 加载对应角色菜单列表树 + */ + @GetMapping(value = "/roleMenuTreeselect/{roleId}") + public AjaxResult roleMenuTreeselect(@PathVariable("roleId") Long roleId) + { + List menus = menuService.selectMenuList(getUserId()); + AjaxResult ajax = AjaxResult.success(); + ajax.put("checkedKeys", menuService.selectMenuListByRoleId(roleId)); + ajax.put("menus", menuService.buildMenuTreeSelect(menus)); + return ajax; + } + + /** + * 新增菜单 + */ + @PreAuthorize("@ss.hasPermi('system:menu:add')") + @Log(title = "菜单管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysMenu menu) + { + if (!menuService.checkMenuNameUnique(menu)) + { + return error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); + } + else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) + { + return error("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); + } + menu.setCreateBy(getUsername()); + return toAjax(menuService.insertMenu(menu)); + } + + /** + * 修改菜单 + */ + @PreAuthorize("@ss.hasPermi('system:menu:edit')") + @Log(title = "菜单管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysMenu menu) + { + if (!menuService.checkMenuNameUnique(menu)) + { + return error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); + } + else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) + { + return error("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); + } + else if (menu.getMenuId().equals(menu.getParentId())) + { + return error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己"); + } + menu.setUpdateBy(getUsername()); + return toAjax(menuService.updateMenu(menu)); + } + + /** + * 删除菜单 + */ + @PreAuthorize("@ss.hasPermi('system:menu:remove')") + @Log(title = "菜单管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{menuId}") + public AjaxResult remove(@PathVariable("menuId") Long menuId) + { + if (menuService.hasChildByMenuId(menuId)) + { + return warn("存在子菜单,不允许删除"); + } + if (menuService.checkMenuExistRole(menuId)) + { + return warn("菜单已分配,不允许删除"); + } + return toAjax(menuService.deleteMenuById(menuId)); + } +} \ No newline at end of file diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysNoticeController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysNoticeController.java new file mode 100644 index 0000000..8622828 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysNoticeController.java @@ -0,0 +1,91 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.system.domain.SysNotice; +import com.ruoyi.system.service.ISysNoticeService; + +/** + * 公告 信息操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/notice") +public class SysNoticeController extends BaseController +{ + @Autowired + private ISysNoticeService noticeService; + + /** + * 获取通知公告列表 + */ + @PreAuthorize("@ss.hasPermi('system:notice:list')") + @GetMapping("/list") + public TableDataInfo list(SysNotice notice) + { + startPage(); + List list = noticeService.selectNoticeList(notice); + return getDataTable(list); + } + + /** + * 根据通知公告编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:notice:query')") + @GetMapping(value = "/{noticeId}") + public AjaxResult getInfo(@PathVariable Long noticeId) + { + return success(noticeService.selectNoticeById(noticeId)); + } + + /** + * 新增通知公告 + */ + @PreAuthorize("@ss.hasPermi('system:notice:add')") + @Log(title = "通知公告", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysNotice notice) + { + notice.setCreateBy(getUsername()); + return toAjax(noticeService.insertNotice(notice)); + } + + /** + * 修改通知公告 + */ + @PreAuthorize("@ss.hasPermi('system:notice:edit')") + @Log(title = "通知公告", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysNotice notice) + { + notice.setUpdateBy(getUsername()); + return toAjax(noticeService.updateNotice(notice)); + } + + /** + * 删除通知公告 + */ + @PreAuthorize("@ss.hasPermi('system:notice:remove')") + @Log(title = "通知公告", businessType = BusinessType.DELETE) + @DeleteMapping("/{noticeIds}") + public AjaxResult remove(@PathVariable Long[] noticeIds) + { + return toAjax(noticeService.deleteNoticeByIds(noticeIds)); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysPostController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysPostController.java new file mode 100644 index 0000000..c37a543 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysPostController.java @@ -0,0 +1,129 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.system.domain.SysPost; +import com.ruoyi.system.service.ISysPostService; + +/** + * 岗位信息操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/post") +public class SysPostController extends BaseController +{ + @Autowired + private ISysPostService postService; + + /** + * 获取岗位列表 + */ + @PreAuthorize("@ss.hasPermi('system:post:list')") + @GetMapping("/list") + public TableDataInfo list(SysPost post) + { + startPage(); + List list = postService.selectPostList(post); + return getDataTable(list); + } + + @Log(title = "岗位管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:post:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysPost post) + { + List list = postService.selectPostList(post); + ExcelUtil util = new ExcelUtil(SysPost.class); + util.exportExcel(response, list, "岗位数据"); + } + + /** + * 根据岗位编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:post:query')") + @GetMapping(value = "/{postId}") + public AjaxResult getInfo(@PathVariable Long postId) + { + return success(postService.selectPostById(postId)); + } + + /** + * 新增岗位 + */ + @PreAuthorize("@ss.hasPermi('system:post:add')") + @Log(title = "岗位管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysPost post) + { + if (!postService.checkPostNameUnique(post)) + { + return error("新增岗位'" + post.getPostName() + "'失败,岗位名称已存在"); + } + else if (!postService.checkPostCodeUnique(post)) + { + return error("新增岗位'" + post.getPostName() + "'失败,岗位编码已存在"); + } + post.setCreateBy(getUsername()); + return toAjax(postService.insertPost(post)); + } + + /** + * 修改岗位 + */ + @PreAuthorize("@ss.hasPermi('system:post:edit')") + @Log(title = "岗位管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysPost post) + { + if (!postService.checkPostNameUnique(post)) + { + return error("修改岗位'" + post.getPostName() + "'失败,岗位名称已存在"); + } + else if (!postService.checkPostCodeUnique(post)) + { + return error("修改岗位'" + post.getPostName() + "'失败,岗位编码已存在"); + } + post.setUpdateBy(getUsername()); + return toAjax(postService.updatePost(post)); + } + + /** + * 删除岗位 + */ + @PreAuthorize("@ss.hasPermi('system:post:remove')") + @Log(title = "岗位管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{postIds}") + public AjaxResult remove(@PathVariable Long[] postIds) + { + return toAjax(postService.deletePostByIds(postIds)); + } + + /** + * 获取岗位选择框列表 + */ + @GetMapping("/optionselect") + public AjaxResult optionselect() + { + List posts = postService.selectPostAll(); + return success(posts); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java new file mode 100644 index 0000000..4c3e10d --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java @@ -0,0 +1,148 @@ +package com.ruoyi.web.controller.system; + +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.file.FileUploadUtils; +import com.ruoyi.common.utils.file.FileUtils; +import com.ruoyi.common.utils.file.MimeTypeUtils; +import com.ruoyi.framework.web.service.TokenService; +import com.ruoyi.system.service.ISysUserService; + +/** + * 个人信息 业务处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/user/profile") +public class SysProfileController extends BaseController +{ + @Autowired + private ISysUserService userService; + + @Autowired + private TokenService tokenService; + + /** + * 个人信息 + */ + @GetMapping + public AjaxResult profile() + { + LoginUser loginUser = getLoginUser(); + SysUser user = loginUser.getUser(); + AjaxResult ajax = AjaxResult.success(user); + ajax.put("roleGroup", userService.selectUserRoleGroup(loginUser.getUsername())); + ajax.put("postGroup", userService.selectUserPostGroup(loginUser.getUsername())); + return ajax; + } + + /** + * 修改用户 + */ + @Log(title = "个人信息", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult updateProfile(@RequestBody SysUser user) + { + LoginUser loginUser = getLoginUser(); + SysUser currentUser = loginUser.getUser(); + currentUser.setNickName(user.getNickName()); + currentUser.setEmail(user.getEmail()); + currentUser.setPhonenumber(user.getPhonenumber()); + currentUser.setSex(user.getSex()); + if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(currentUser)) + { + return error("修改用户'" + loginUser.getUsername() + "'失败,手机号码已存在"); + } + if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(currentUser)) + { + return error("修改用户'" + loginUser.getUsername() + "'失败,邮箱账号已存在"); + } + if (userService.updateUserProfile(currentUser) > 0) + { + // 更新缓存用户信息 + tokenService.setLoginUser(loginUser); + return success(); + } + return error("修改个人信息异常,请联系管理员"); + } + + /** + * 重置密码 + */ + @Log(title = "个人信息", businessType = BusinessType.UPDATE) + @PutMapping("/updatePwd") + public AjaxResult updatePwd(@RequestBody Map params) + { + String oldPassword = params.get("oldPassword"); + String newPassword = params.get("newPassword"); + LoginUser loginUser = getLoginUser(); + Long userId = loginUser.getUserId(); + String password = loginUser.getPassword(); + if (!SecurityUtils.matchesPassword(oldPassword, password)) + { + return error("修改密码失败,旧密码错误"); + } + if (SecurityUtils.matchesPassword(newPassword, password)) + { + return error("新密码不能与旧密码相同"); + } + newPassword = SecurityUtils.encryptPassword(newPassword); + if (userService.resetUserPwd(userId, newPassword) > 0) + { + // 更新缓存用户密码&密码最后更新时间 + loginUser.getUser().setPwdUpdateDate(DateUtils.getNowDate()); + loginUser.getUser().setPassword(newPassword); + tokenService.setLoginUser(loginUser); + return success(); + } + return error("修改密码异常,请联系管理员"); + } + + /** + * 头像上传 + */ + @Log(title = "用户头像", businessType = BusinessType.UPDATE) + @PostMapping("/avatar") + public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws Exception + { + if (!file.isEmpty()) + { + LoginUser loginUser = getLoginUser(); + String avatar = FileUploadUtils.upload(RuoYiConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION, true); + if (userService.updateUserAvatar(loginUser.getUserId(), avatar)) + { + String oldAvatar = loginUser.getUser().getAvatar(); + if (StringUtils.isNotEmpty(oldAvatar)) + { + FileUtils.deleteFile(RuoYiConfig.getProfile() + FileUtils.stripPrefix(oldAvatar)); + } + AjaxResult ajax = AjaxResult.success(); + ajax.put("imgUrl", avatar); + // 更新缓存用户头像 + loginUser.getUser().setAvatar(avatar); + tokenService.setLoginUser(loginUser); + return ajax; + } + } + return error("上传图片异常,请联系管理员"); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java new file mode 100644 index 0000000..fe19249 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java @@ -0,0 +1,38 @@ +package com.ruoyi.web.controller.system; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.model.RegisterBody; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.web.service.SysRegisterService; +import com.ruoyi.system.service.ISysConfigService; + +/** + * 注册验证 + * + * @author ruoyi + */ +@RestController +public class SysRegisterController extends BaseController +{ + @Autowired + private SysRegisterService registerService; + + @Autowired + private ISysConfigService configService; + + @PostMapping("/register") + public AjaxResult register(@RequestBody RegisterBody user) + { + if (!("true".equals(configService.selectConfigByKey("sys.account.registerUser")))) + { + return error("当前系统没有开启注册功能!"); + } + String msg = registerService.register(user); + return StringUtils.isEmpty(msg) ? success() : error(msg); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java new file mode 100644 index 0000000..42d9e8f --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java @@ -0,0 +1,262 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysDept; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.framework.web.service.SysPermissionService; +import com.ruoyi.framework.web.service.TokenService; +import com.ruoyi.system.domain.SysUserRole; +import com.ruoyi.system.service.ISysDeptService; +import com.ruoyi.system.service.ISysRoleService; +import com.ruoyi.system.service.ISysUserService; + +/** + * 角色信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/role") +public class SysRoleController extends BaseController +{ + @Autowired + private ISysRoleService roleService; + + @Autowired + private TokenService tokenService; + + @Autowired + private SysPermissionService permissionService; + + @Autowired + private ISysUserService userService; + + @Autowired + private ISysDeptService deptService; + + @PreAuthorize("@ss.hasPermi('system:role:list')") + @GetMapping("/list") + public TableDataInfo list(SysRole role) + { + startPage(); + List list = roleService.selectRoleList(role); + return getDataTable(list); + } + + @Log(title = "角色管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:role:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysRole role) + { + List list = roleService.selectRoleList(role); + ExcelUtil util = new ExcelUtil(SysRole.class); + util.exportExcel(response, list, "角色数据"); + } + + /** + * 根据角色编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:role:query')") + @GetMapping(value = "/{roleId}") + public AjaxResult getInfo(@PathVariable Long roleId) + { + roleService.checkRoleDataScope(roleId); + return success(roleService.selectRoleById(roleId)); + } + + /** + * 新增角色 + */ + @PreAuthorize("@ss.hasPermi('system:role:add')") + @Log(title = "角色管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysRole role) + { + if (!roleService.checkRoleNameUnique(role)) + { + return error("新增角色'" + role.getRoleName() + "'失败,角色名称已存在"); + } + else if (!roleService.checkRoleKeyUnique(role)) + { + return error("新增角色'" + role.getRoleName() + "'失败,角色权限已存在"); + } + role.setCreateBy(getUsername()); + return toAjax(roleService.insertRole(role)); + + } + + /** + * 修改保存角色 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysRole role) + { + roleService.checkRoleAllowed(role); + roleService.checkRoleDataScope(role.getRoleId()); + if (!roleService.checkRoleNameUnique(role)) + { + return error("修改角色'" + role.getRoleName() + "'失败,角色名称已存在"); + } + else if (!roleService.checkRoleKeyUnique(role)) + { + return error("修改角色'" + role.getRoleName() + "'失败,角色权限已存在"); + } + role.setUpdateBy(getUsername()); + + if (roleService.updateRole(role) > 0) + { + // 更新缓存用户权限 + LoginUser loginUser = getLoginUser(); + if (StringUtils.isNotNull(loginUser.getUser()) && !loginUser.getUser().isAdmin()) + { + loginUser.setUser(userService.selectUserByUserName(loginUser.getUser().getUserName())); + loginUser.setPermissions(permissionService.getMenuPermission(loginUser.getUser())); + tokenService.setLoginUser(loginUser); + } + return success(); + } + return error("修改角色'" + role.getRoleName() + "'失败,请联系管理员"); + } + + /** + * 修改保存数据权限 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.UPDATE) + @PutMapping("/dataScope") + public AjaxResult dataScope(@RequestBody SysRole role) + { + roleService.checkRoleAllowed(role); + roleService.checkRoleDataScope(role.getRoleId()); + return toAjax(roleService.authDataScope(role)); + } + + /** + * 状态修改 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysRole role) + { + roleService.checkRoleAllowed(role); + roleService.checkRoleDataScope(role.getRoleId()); + role.setUpdateBy(getUsername()); + return toAjax(roleService.updateRoleStatus(role)); + } + + /** + * 删除角色 + */ + @PreAuthorize("@ss.hasPermi('system:role:remove')") + @Log(title = "角色管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{roleIds}") + public AjaxResult remove(@PathVariable Long[] roleIds) + { + return toAjax(roleService.deleteRoleByIds(roleIds)); + } + + /** + * 获取角色选择框列表 + */ + @PreAuthorize("@ss.hasPermi('system:role:query')") + @GetMapping("/optionselect") + public AjaxResult optionselect() + { + return success(roleService.selectRoleAll()); + } + + /** + * 查询已分配用户角色列表 + */ + @PreAuthorize("@ss.hasPermi('system:role:list')") + @GetMapping("/authUser/allocatedList") + public TableDataInfo allocatedList(SysUser user) + { + startPage(); + List list = userService.selectAllocatedList(user); + return getDataTable(list); + } + + /** + * 查询未分配用户角色列表 + */ + @PreAuthorize("@ss.hasPermi('system:role:list')") + @GetMapping("/authUser/unallocatedList") + public TableDataInfo unallocatedList(SysUser user) + { + startPage(); + List list = userService.selectUnallocatedList(user); + return getDataTable(list); + } + + /** + * 取消授权用户 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.GRANT) + @PutMapping("/authUser/cancel") + public AjaxResult cancelAuthUser(@RequestBody SysUserRole userRole) + { + return toAjax(roleService.deleteAuthUser(userRole)); + } + + /** + * 批量取消授权用户 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.GRANT) + @PutMapping("/authUser/cancelAll") + public AjaxResult cancelAuthUserAll(Long roleId, Long[] userIds) + { + return toAjax(roleService.deleteAuthUsers(roleId, userIds)); + } + + /** + * 批量选择用户授权 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.GRANT) + @PutMapping("/authUser/selectAll") + public AjaxResult selectAuthUserAll(Long roleId, Long[] userIds) + { + roleService.checkRoleDataScope(roleId); + return toAjax(roleService.insertAuthUsers(roleId, userIds)); + } + + /** + * 获取对应角色部门树列表 + */ + @PreAuthorize("@ss.hasPermi('system:role:query')") + @GetMapping(value = "/deptTree/{roleId}") + public AjaxResult deptTree(@PathVariable("roleId") Long roleId) + { + AjaxResult ajax = AjaxResult.success(); + ajax.put("checkedKeys", deptService.selectDeptListByRoleId(roleId)); + ajax.put("depts", deptService.selectDeptTreeList(new SysDept())); + return ajax; + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java new file mode 100644 index 0000000..130c438 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java @@ -0,0 +1,256 @@ +package com.ruoyi.web.controller.system; + +import java.util.List; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysDept; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.system.service.ISysDeptService; +import com.ruoyi.system.service.ISysPostService; +import com.ruoyi.system.service.ISysRoleService; +import com.ruoyi.system.service.ISysUserService; + +/** + * 用户信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/user") +public class SysUserController extends BaseController +{ + @Autowired + private ISysUserService userService; + + @Autowired + private ISysRoleService roleService; + + @Autowired + private ISysDeptService deptService; + + @Autowired + private ISysPostService postService; + + /** + * 获取用户列表 + */ + @PreAuthorize("@ss.hasPermi('system:user:list')") + @GetMapping("/list") + public TableDataInfo list(SysUser user) + { + startPage(); + List list = userService.selectUserList(user); + return getDataTable(list); + } + + @Log(title = "用户管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:user:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysUser user) + { + List list = userService.selectUserList(user); + ExcelUtil util = new ExcelUtil(SysUser.class); + util.exportExcel(response, list, "用户数据"); + } + + @Log(title = "用户管理", businessType = BusinessType.IMPORT) + @PreAuthorize("@ss.hasPermi('system:user:import')") + @PostMapping("/importData") + public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception + { + ExcelUtil util = new ExcelUtil(SysUser.class); + List userList = util.importExcel(file.getInputStream()); + String operName = getUsername(); + String message = userService.importUser(userList, updateSupport, operName); + return success(message); + } + + @PostMapping("/importTemplate") + public void importTemplate(HttpServletResponse response) + { + ExcelUtil util = new ExcelUtil(SysUser.class); + util.importTemplateExcel(response, "用户数据"); + } + + /** + * 根据用户编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:user:query')") + @GetMapping(value = { "/", "/{userId}" }) + public AjaxResult getInfo(@PathVariable(value = "userId", required = false) Long userId) + { + AjaxResult ajax = AjaxResult.success(); + if (StringUtils.isNotNull(userId)) + { + userService.checkUserDataScope(userId); + SysUser sysUser = userService.selectUserById(userId); + ajax.put(AjaxResult.DATA_TAG, sysUser); + ajax.put("postIds", postService.selectPostListByUserId(userId)); + ajax.put("roleIds", sysUser.getRoles().stream().map(SysRole::getRoleId).collect(Collectors.toList())); + } + List roles = roleService.selectRoleAll(); + ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList())); + ajax.put("posts", postService.selectPostAll()); + return ajax; + } + + /** + * 新增用户 + */ + @PreAuthorize("@ss.hasPermi('system:user:add')") + @Log(title = "用户管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysUser user) + { + deptService.checkDeptDataScope(user.getDeptId()); + roleService.checkRoleDataScope(user.getRoleIds()); + if (!userService.checkUserNameUnique(user)) + { + return error("新增用户'" + user.getUserName() + "'失败,登录账号已存在"); + } + else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) + { + return error("新增用户'" + user.getUserName() + "'失败,手机号码已存在"); + } + else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user)) + { + return error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在"); + } + user.setCreateBy(getUsername()); + user.setPassword(SecurityUtils.encryptPassword(user.getPassword())); + return toAjax(userService.insertUser(user)); + } + + /** + * 修改用户 + */ + @PreAuthorize("@ss.hasPermi('system:user:edit')") + @Log(title = "用户管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysUser user) + { + userService.checkUserAllowed(user); + userService.checkUserDataScope(user.getUserId()); + deptService.checkDeptDataScope(user.getDeptId()); + roleService.checkRoleDataScope(user.getRoleIds()); + if (!userService.checkUserNameUnique(user)) + { + return error("修改用户'" + user.getUserName() + "'失败,登录账号已存在"); + } + else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) + { + return error("修改用户'" + user.getUserName() + "'失败,手机号码已存在"); + } + else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user)) + { + return error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在"); + } + user.setUpdateBy(getUsername()); + return toAjax(userService.updateUser(user)); + } + + /** + * 删除用户 + */ + @PreAuthorize("@ss.hasPermi('system:user:remove')") + @Log(title = "用户管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{userIds}") + public AjaxResult remove(@PathVariable Long[] userIds) + { + if (ArrayUtils.contains(userIds, getUserId())) + { + return error("当前用户不能删除"); + } + return toAjax(userService.deleteUserByIds(userIds)); + } + + /** + * 重置密码 + */ + @PreAuthorize("@ss.hasPermi('system:user:resetPwd')") + @Log(title = "用户管理", businessType = BusinessType.UPDATE) + @PutMapping("/resetPwd") + public AjaxResult resetPwd(@RequestBody SysUser user) + { + userService.checkUserAllowed(user); + userService.checkUserDataScope(user.getUserId()); + user.setPassword(SecurityUtils.encryptPassword(user.getPassword())); + user.setUpdateBy(getUsername()); + return toAjax(userService.resetPwd(user)); + } + + /** + * 状态修改 + */ + @PreAuthorize("@ss.hasPermi('system:user:edit')") + @Log(title = "用户管理", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysUser user) + { + userService.checkUserAllowed(user); + userService.checkUserDataScope(user.getUserId()); + user.setUpdateBy(getUsername()); + return toAjax(userService.updateUserStatus(user)); + } + + /** + * 根据用户编号获取授权角色 + */ + @PreAuthorize("@ss.hasPermi('system:user:query')") + @GetMapping("/authRole/{userId}") + public AjaxResult authRole(@PathVariable("userId") Long userId) + { + AjaxResult ajax = AjaxResult.success(); + SysUser user = userService.selectUserById(userId); + List roles = roleService.selectRolesByUserId(userId); + ajax.put("user", user); + ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList())); + return ajax; + } + + /** + * 用户授权角色 + */ + @PreAuthorize("@ss.hasPermi('system:user:edit')") + @Log(title = "用户管理", businessType = BusinessType.GRANT) + @PutMapping("/authRole") + public AjaxResult insertAuthRole(Long userId, Long[] roleIds) + { + userService.checkUserDataScope(userId); + roleService.checkRoleDataScope(roleIds); + userService.insertUserAuth(userId, roleIds); + return success(); + } + + /** + * 获取部门树列表 + */ + @PreAuthorize("@ss.hasPermi('system:user:list')") + @GetMapping("/deptTree") + public AjaxResult deptTree(SysDept dept) + { + return success(deptService.selectDeptTreeList(dept)); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/TestController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/TestController.java new file mode 100644 index 0000000..b4f6bac --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/TestController.java @@ -0,0 +1,183 @@ +package com.ruoyi.web.controller.tool; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.utils.StringUtils; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiOperation; + +/** + * swagger 用户测试方法 + * + * @author ruoyi + */ +@Api("用户信息管理") +@RestController +@RequestMapping("/test/user") +public class TestController extends BaseController +{ + private final static Map users = new LinkedHashMap(); + { + users.put(1, new UserEntity(1, "admin", "admin123", "15888888888")); + users.put(2, new UserEntity(2, "ry", "admin123", "15666666666")); + } + + @ApiOperation("获取用户列表") + @GetMapping("/list") + public R> userList() + { + List userList = new ArrayList(users.values()); + return R.ok(userList); + } + + @ApiOperation("获取用户详细") + @ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "int", paramType = "path", dataTypeClass = Integer.class) + @GetMapping("/{userId}") + public R getUser(@PathVariable Integer userId) + { + if (!users.isEmpty() && users.containsKey(userId)) + { + return R.ok(users.get(userId)); + } + else + { + return R.fail("用户不存在"); + } + } + + @ApiOperation("新增用户") + @ApiImplicitParams({ + @ApiImplicitParam(name = "userId", value = "用户id", dataType = "Integer", dataTypeClass = Integer.class), + @ApiImplicitParam(name = "username", value = "用户名称", dataType = "String", dataTypeClass = String.class), + @ApiImplicitParam(name = "password", value = "用户密码", dataType = "String", dataTypeClass = String.class), + @ApiImplicitParam(name = "mobile", value = "用户手机", dataType = "String", dataTypeClass = String.class) + }) + @PostMapping("/save") + public R save(UserEntity user) + { + if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId())) + { + return R.fail("用户ID不能为空"); + } + users.put(user.getUserId(), user); + return R.ok(); + } + + @ApiOperation("更新用户") + @PutMapping("/update") + public R update(@RequestBody UserEntity user) + { + if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId())) + { + return R.fail("用户ID不能为空"); + } + if (users.isEmpty() || !users.containsKey(user.getUserId())) + { + return R.fail("用户不存在"); + } + users.remove(user.getUserId()); + users.put(user.getUserId(), user); + return R.ok(); + } + + @ApiOperation("删除用户信息") + @ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "int", paramType = "path", dataTypeClass = Integer.class) + @DeleteMapping("/{userId}") + public R delete(@PathVariable Integer userId) + { + if (!users.isEmpty() && users.containsKey(userId)) + { + users.remove(userId); + return R.ok(); + } + else + { + return R.fail("用户不存在"); + } + } +} + +@ApiModel(value = "UserEntity", description = "用户实体") +class UserEntity +{ + @ApiModelProperty("用户ID") + private Integer userId; + + @ApiModelProperty("用户名称") + private String username; + + @ApiModelProperty("用户密码") + private String password; + + @ApiModelProperty("用户手机") + private String mobile; + + public UserEntity() + { + + } + + public UserEntity(Integer userId, String username, String password, String mobile) + { + this.userId = userId; + this.username = username; + this.password = password; + this.mobile = mobile; + } + + public Integer getUserId() + { + return userId; + } + + public void setUserId(Integer userId) + { + this.userId = userId; + } + + public String getUsername() + { + return username; + } + + public void setUsername(String username) + { + this.username = username; + } + + public String getPassword() + { + return password; + } + + public void setPassword(String password) + { + this.password = password; + } + + public String getMobile() + { + return mobile; + } + + public void setMobile(String mobile) + { + this.mobile = mobile; + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/core/config/SwaggerConfig.java b/ruoyi-admin/src/main/java/com/ruoyi/web/core/config/SwaggerConfig.java new file mode 100644 index 0000000..ae1c3ec --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/core/config/SwaggerConfig.java @@ -0,0 +1,125 @@ +package com.ruoyi.web.core.config; + +import java.util.ArrayList; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import com.ruoyi.common.config.RuoYiConfig; +import io.swagger.annotations.ApiOperation; +import io.swagger.models.auth.In; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.ApiKey; +import springfox.documentation.service.AuthorizationScope; +import springfox.documentation.service.Contact; +import springfox.documentation.service.SecurityReference; +import springfox.documentation.service.SecurityScheme; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.contexts.SecurityContext; +import springfox.documentation.spring.web.plugins.Docket; + +/** + * Swagger2的接口配置 + * + * @author ruoyi + */ +@Configuration +public class SwaggerConfig +{ + /** 系统基础配置 */ + @Autowired + private RuoYiConfig ruoyiConfig; + + /** 是否开启swagger */ + @Value("${swagger.enabled}") + private boolean enabled; + + /** 设置请求的统一前缀 */ + @Value("${swagger.pathMapping}") + private String pathMapping; + + /** + * 创建API + */ + @Bean + public Docket createRestApi() + { + return new Docket(DocumentationType.OAS_30) + // 是否启用Swagger + .enable(enabled) + // 用来创建该API的基本信息,展示在文档的页面中(自定义展示的信息) + .apiInfo(apiInfo()) + // 设置哪些接口暴露给Swagger展示 + .select() + // 扫描所有有注解的api,用这种方式更灵活 + .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) + // 扫描指定包中的swagger注解 + // .apis(RequestHandlerSelectors.basePackage("com.ruoyi.project.tool.swagger")) + // 扫描所有 .apis(RequestHandlerSelectors.any()) + .paths(PathSelectors.any()) + .build() + /* 设置安全模式,swagger可以设置访问token */ + .securitySchemes(securitySchemes()) + .securityContexts(securityContexts()) + .pathMapping(pathMapping); + } + + /** + * 安全模式,这里指定token通过Authorization头请求头传递 + */ + private List securitySchemes() + { + List apiKeyList = new ArrayList(); + apiKeyList.add(new ApiKey("Authorization", "Authorization", In.HEADER.toValue())); + return apiKeyList; + } + + /** + * 安全上下文 + */ + private List securityContexts() + { + List securityContexts = new ArrayList<>(); + securityContexts.add( + SecurityContext.builder() + .securityReferences(defaultAuth()) + .operationSelector(o -> o.requestMappingPattern().matches("/.*")) + .build()); + return securityContexts; + } + + /** + * 默认的安全上引用 + */ + private List defaultAuth() + { + AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything"); + AuthorizationScope[] authorizationScopes = new AuthorizationScope[1]; + authorizationScopes[0] = authorizationScope; + List securityReferences = new ArrayList<>(); + securityReferences.add(new SecurityReference("Authorization", authorizationScopes)); + return securityReferences; + } + + /** + * 添加摘要信息 + */ + private ApiInfo apiInfo() + { + // 用ApiInfoBuilder进行定制 + return new ApiInfoBuilder() + // 设置标题 + .title("标题:若依管理系统_接口文档") + // 描述 + .description("描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...") + // 作者信息 + .contact(new Contact(ruoyiConfig.getName(), null, null)) + // 版本 + .version("版本号:" + ruoyiConfig.getVersion()) + .build(); + } +} diff --git a/ruoyi-admin/src/main/resources/META-INF/spring-devtools.properties b/ruoyi-admin/src/main/resources/META-INF/spring-devtools.properties new file mode 100644 index 0000000..37e7b58 --- /dev/null +++ b/ruoyi-admin/src/main/resources/META-INF/spring-devtools.properties @@ -0,0 +1 @@ +restart.include.json=/com.alibaba.fastjson2.*.jar \ No newline at end of file diff --git a/ruoyi-admin/src/main/resources/application-druid.yml b/ruoyi-admin/src/main/resources/application-druid.yml new file mode 100644 index 0000000..f145fdd --- /dev/null +++ b/ruoyi-admin/src/main/resources/application-druid.yml @@ -0,0 +1,61 @@ +# 数据源配置 +spring: + datasource: + type: com.alibaba.druid.pool.DruidDataSource + driverClassName: com.mysql.cj.jdbc.Driver + druid: + # 主库数据源 + master: + url: jdbc:mysql://192.168.0.119:3318/rongxin_base?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 + username: nsbjgkwh + password: ns61GK32x% + # 从库数据源 + slave: + # 从数据源开关/默认关闭 + enabled: false + url: + username: + password: + # 初始连接数 + initialSize: 5 + # 最小连接池数量 + minIdle: 10 + # 最大连接池数量 + maxActive: 20 + # 配置获取连接等待超时的时间 + maxWait: 60000 + # 配置连接超时时间 + connectTimeout: 30000 + # 配置网络超时时间 + socketTimeout: 60000 + # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 + timeBetweenEvictionRunsMillis: 60000 + # 配置一个连接在池中最小生存的时间,单位是毫秒 + minEvictableIdleTimeMillis: 300000 + # 配置一个连接在池中最大生存的时间,单位是毫秒 + maxEvictableIdleTimeMillis: 900000 + # 配置检测连接是否有效 + validationQuery: SELECT 1 FROM DUAL + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + webStatFilter: + enabled: true + statViewServlet: + enabled: true + # 设置白名单,不填则允许所有访问 + allow: + url-pattern: /druid/* + # 控制台管理用户名和密码 + login-username: ruoyi + login-password: 123456 + filter: + stat: + enabled: true + # 慢SQL记录 + log-slow-sql: true + slow-sql-millis: 1000 + merge-sql: true + wall: + config: + multi-statement-allow: true diff --git a/ruoyi-admin/src/main/resources/application-third.yml b/ruoyi-admin/src/main/resources/application-third.yml new file mode 100644 index 0000000..e69de29 diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml new file mode 100644 index 0000000..b555ae0 --- /dev/null +++ b/ruoyi-admin/src/main/resources/application.yml @@ -0,0 +1,131 @@ +# 项目相关配置 +ruoyi: + # 名称 + name: RuoYi + # 版本 + version: 3.9.0 + # 版权年份 + copyrightYear: 2025 + # 文件路径 示例( Windows配置D:/rongxin/demo/uploadPath,Linux配置 /home/rongxin/demo/uploadPath) + profile: D:/rongxin/demo/uploadPath + # 日志输出路径, 不设置则使用logback.xml配置的 + logPath: D:/rongxin/demo/logs + # 获取ip地址开关 + addressEnabled: false + # 验证码类型 math 数字计算 char 字符验证 + captchaType: math + +# 开发环境配置 +server: + # 服务器的HTTP端口,默认为8080 + port: 8080 + servlet: + # 应用的访问路径 + context-path: / + tomcat: + # tomcat的URI编码 + uri-encoding: UTF-8 + # 连接数满后的排队数,默认为100 + accept-count: 1000 + threads: + # tomcat最大线程数,默认为200 + max: 800 + # Tomcat启动初始化的线程数,默认值10 + min-spare: 100 + +# 日志配置 +logging: + level: + com.ruoyi: debug + org.springframework: warn + +# 用户配置 +user: + password: + # 密码最大错误次数 + maxRetryCount: 5 + # 密码锁定时间(默认10分钟) + lockTime: 10 + +# Spring配置 +spring: + # 资源信息 + messages: + # 国际化资源文件路径 + basename: i18n/messages + profiles: + active: druid,third + # 文件上传 + servlet: + multipart: + # 单个文件大小 + max-file-size: 10MB + # 设置总上传的文件大小 + max-request-size: 20MB + # 服务模块 + devtools: + restart: + # 热部署开关 + enabled: true + # redis 配置 + redis: + # 地址 + host: localhost + # 端口,默认为6379 + port: 6379 + # 数据库索引 + database: 0 + # 密码 + password: + # 连接超时时间 + timeout: 10s + lettuce: + pool: + # 连接池中的最小空闲连接 + min-idle: 0 + # 连接池中的最大空闲连接 + max-idle: 8 + # 连接池的最大数据库连接数 + max-active: 8 + # #连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + +# token配置 +token: + # 令牌自定义标识 + header: Authorization + # 令牌密钥 + secret: abcdefghijklmnopqrstuvwxyz + # 令牌有效期(默认30分钟) + expireTime: 30 + +# MyBatis配置 +mybatis: + # 搜索指定包别名 + typeAliasesPackage: com.ruoyi.**.domain + # 配置mapper的扫描,找到所有的mapper.xml映射文件 + mapperLocations: classpath*:mapper/**/*Mapper.xml + # 加载全局的配置文件 + configLocation: classpath:mybatis/mybatis-config.xml + +# PageHelper分页插件 +pagehelper: + helperDialect: mysql + supportMethodsArguments: true + params: count=countSql + +# Swagger配置 +swagger: + # 是否开启swagger + enabled: true + # 请求前缀 + pathMapping: /dev-api + +# 防止XSS攻击 +xss: + # 过滤开关 + enabled: true + # 排除链接(多个用逗号分隔) + excludes: /system/notice + # 匹配链接 + urlPatterns: /system/*,/monitor/*,/tool/* diff --git a/ruoyi-admin/src/main/resources/banner.txt b/ruoyi-admin/src/main/resources/banner.txt new file mode 100644 index 0000000..0931cb8 --- /dev/null +++ b/ruoyi-admin/src/main/resources/banner.txt @@ -0,0 +1,24 @@ +Application Version: ${ruoyi.version} +Spring Boot Version: ${spring-boot.version} +//////////////////////////////////////////////////////////////////// +// _ooOoo_ // +// o8888888o // +// 88" . "88 // +// (| ^_^ |) // +// O\ = /O // +// ____/`---'\____ // +// .' \\| |// `. // +// / \\||| : |||// \ // +// / _||||| -:- |||||- \ // +// | | \\\ - /// | | // +// | \_| ''\---/'' | | // +// \ .-\__ `-` ___/-. / // +// ___`. .' /--.--\ `. . ___ // +// ."" '< `.___\_<|>_/___.' >'"". // +// | | : `- \`.;`\ _ /`;.`/ - ` : | | // +// \ \ `-. \_ __\ /__ _/ .-` / / // +// ========`-.____`-.___\_____/___.-`____.-'======== // +// `=---=' // +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // +// 佛祖保佑 永不宕机 永无BUG // +//////////////////////////////////////////////////////////////////// \ No newline at end of file diff --git a/ruoyi-admin/src/main/resources/i18n/messages.properties b/ruoyi-admin/src/main/resources/i18n/messages.properties new file mode 100644 index 0000000..93de005 --- /dev/null +++ b/ruoyi-admin/src/main/resources/i18n/messages.properties @@ -0,0 +1,38 @@ +#错误消息 +not.null=* 必须填写 +user.jcaptcha.error=验证码错误 +user.jcaptcha.expire=验证码已失效 +user.not.exists=用户不存在/密码错误 +user.password.not.match=用户不存在/密码错误 +user.password.retry.limit.count=密码输入错误{0}次 +user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟 +user.password.delete=对不起,您的账号已被删除 +user.blocked=用户已封禁,请联系管理员 +role.blocked=角色已封禁,请联系管理员 +login.blocked=很遗憾,访问IP已被列入系统黑名单 +user.logout.success=退出成功 + +length.not.valid=长度必须在{min}到{max}个字符之间 + +user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头 +user.password.not.valid=* 5-50个字符 + +user.email.not.valid=邮箱格式错误 +user.mobile.phone.number.not.valid=手机号格式错误 +user.login.success=登录成功 +user.register.success=注册成功 +user.notfound=请重新登录 +user.forcelogout=管理员强制退出,请重新登录 +user.unknown.error=未知错误,请重新登录 + +##文件上传消息 +upload.exceed.maxSize=上传的文件大小超出限制的文件大小!
允许的文件最大大小是:{0}MB! +upload.filename.exceed.length=上传的文件名最长{0}个字符 + +##权限 +no.permission=您没有数据的权限,请联系管理员添加权限 [{0}] +no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}] +no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}] +no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}] +no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}] +no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}] diff --git a/ruoyi-admin/src/main/resources/logback.xml b/ruoyi-admin/src/main/resources/logback.xml new file mode 100644 index 0000000..d249a7d --- /dev/null +++ b/ruoyi-admin/src/main/resources/logback.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + ${log.pattern} + + + + + + ${log.path}/sys-info.log + + + + ${log.path}/sys-info.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + INFO + + ACCEPT + + DENY + + + + + ${log.path}/sys-error.log + + + + ${log.path}/sys-error.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + ERROR + + ACCEPT + + DENY + + + + + + ${log.path}/sys-user.log + + + ${log.path}/sys-user.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ruoyi-admin/src/main/resources/mybatis/mybatis-config.xml b/ruoyi-admin/src/main/resources/mybatis/mybatis-config.xml new file mode 100644 index 0000000..ac47c03 --- /dev/null +++ b/ruoyi-admin/src/main/resources/mybatis/mybatis-config.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + diff --git a/ruoyi-business/pom.xml b/ruoyi-business/pom.xml new file mode 100644 index 0000000..30d08cd --- /dev/null +++ b/ruoyi-business/pom.xml @@ -0,0 +1,15 @@ + + + + ruoyi + com.ruoyi + 3.9.0 + + 4.0.0 + + ruoyi-business + + + diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml new file mode 100644 index 0000000..72d888f --- /dev/null +++ b/ruoyi-common/pom.xml @@ -0,0 +1,124 @@ + + + + ruoyi + com.ruoyi + 3.9.0 + + 4.0.0 + + ruoyi-common + + + common通用工具 + + + + + + + org.springframework + spring-context-support + + + + + org.springframework + spring-web + + + + + org.springframework.boot + spring-boot-starter-security + + + + + com.github.pagehelper + pagehelper-spring-boot-starter + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.apache.commons + commons-lang3 + + + + + com.fasterxml.jackson.core + jackson-databind + + + + + com.alibaba.fastjson2 + fastjson2 + + + + + commons-io + commons-io + + + + + org.apache.poi + poi-ooxml + + + + + org.yaml + snakeyaml + + + + + io.jsonwebtoken + jjwt + + + + + javax.xml.bind + jaxb-api + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.apache.commons + commons-pool2 + + + + + eu.bitwalker + UserAgentUtils + + + + + javax.servlet + javax.servlet-api + + + + + \ No newline at end of file diff --git a/ruoyi-common/ruoyi-common.iml b/ruoyi-common/ruoyi-common.iml new file mode 100644 index 0000000..a7761d3 --- /dev/null +++ b/ruoyi-common/ruoyi-common.iml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Anonymous.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Anonymous.java new file mode 100644 index 0000000..1d6d4f4 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Anonymous.java @@ -0,0 +1,19 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 匿名访问不鉴权注解 + * + * @author ruoyi + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Anonymous +{ +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataScope.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataScope.java new file mode 100644 index 0000000..be49c80 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataScope.java @@ -0,0 +1,33 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 数据权限过滤注解 + * + * @author ruoyi + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DataScope +{ + /** + * 部门表的别名 + */ + public String deptAlias() default ""; + + /** + * 用户表的别名 + */ + public String userAlias() default ""; + + /** + * 权限字符(用于多个角色匹配符合要求的权限)默认根据权限注解@ss获取,多个权限用逗号分隔开来 + */ + public String permission() default ""; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSource.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSource.java new file mode 100644 index 0000000..79cd191 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSource.java @@ -0,0 +1,28 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import com.ruoyi.common.enums.DataSourceType; + +/** + * 自定义多数据源切换注解 + * + * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准 + * + * @author ruoyi + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface DataSource +{ + /** + * 切换数据源名称 + */ + public DataSourceType value() default DataSourceType.MASTER; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excel.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excel.java new file mode 100644 index 0000000..765d8e3 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excel.java @@ -0,0 +1,197 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.math.BigDecimal; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.IndexedColors; +import com.ruoyi.common.utils.poi.ExcelHandlerAdapter; + +/** + * 自定义导出Excel数据注解 + * + * @author ruoyi + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Excel +{ + /** + * 导出时在excel中排序 + */ + public int sort() default Integer.MAX_VALUE; + + /** + * 导出到Excel中的名字. + */ + public String name() default ""; + + /** + * 日期格式, 如: yyyy-MM-dd + */ + public String dateFormat() default ""; + + /** + * 如果是字典类型,请设置字典的type值 (如: sys_user_sex) + */ + public String dictType() default ""; + + /** + * 读取内容转表达式 (如: 0=男,1=女,2=未知) + */ + public String readConverterExp() default ""; + + /** + * 分隔符,读取字符串组内容 + */ + public String separator() default ","; + + /** + * BigDecimal 精度 默认:-1(默认不开启BigDecimal格式化) + */ + public int scale() default -1; + + /** + * BigDecimal 舍入规则 默认:BigDecimal.ROUND_HALF_EVEN + */ + public int roundingMode() default BigDecimal.ROUND_HALF_EVEN; + + /** + * 导出时在excel中每个列的高度 + */ + public double height() default 14; + + /** + * 导出时在excel中每个列的宽度 + */ + public double width() default 16; + + /** + * 文字后缀,如% 90 变成90% + */ + public String suffix() default ""; + + /** + * 当值为空时,字段的默认值 + */ + public String defaultValue() default ""; + + /** + * 提示信息 + */ + public String prompt() default ""; + + /** + * 是否允许内容换行 + */ + public boolean wrapText() default false; + + /** + * 设置只能选择不能输入的列内容. + */ + public String[] combo() default {}; + + /** + * 是否从字典读数据到combo,默认不读取,如读取需要设置dictType注解. + */ + public boolean comboReadDict() default false; + + /** + * 是否需要纵向合并单元格,应对需求:含有list集合单元格) + */ + public boolean needMerge() default false; + + /** + * 是否导出数据,应对需求:有时我们需要导出一份模板,这是标题需要但内容需要用户手工填写. + */ + public boolean isExport() default true; + + /** + * 另一个类中的属性名称,支持多级获取,以小数点隔开 + */ + public String targetAttr() default ""; + + /** + * 是否自动统计数据,在最后追加一行统计数据总和 + */ + public boolean isStatistics() default false; + + /** + * 导出类型(0数字 1字符串 2图片) + */ + public ColumnType cellType() default ColumnType.STRING; + + /** + * 导出列头背景颜色 + */ + public IndexedColors headerBackgroundColor() default IndexedColors.GREY_50_PERCENT; + + /** + * 导出列头字体颜色 + */ + public IndexedColors headerColor() default IndexedColors.WHITE; + + /** + * 导出单元格背景颜色 + */ + public IndexedColors backgroundColor() default IndexedColors.WHITE; + + /** + * 导出单元格字体颜色 + */ + public IndexedColors color() default IndexedColors.BLACK; + + /** + * 导出字段对齐方式 + */ + public HorizontalAlignment align() default HorizontalAlignment.CENTER; + + /** + * 自定义数据处理器 + */ + public Class handler() default ExcelHandlerAdapter.class; + + /** + * 自定义数据处理器参数 + */ + public String[] args() default {}; + + /** + * 字段类型(0:导出导入;1:仅导出;2:仅导入) + */ + Type type() default Type.ALL; + + public enum Type + { + ALL(0), EXPORT(1), IMPORT(2); + private final int value; + + Type(int value) + { + this.value = value; + } + + public int value() + { + return this.value; + } + } + + public enum ColumnType + { + NUMERIC(0), STRING(1), IMAGE(2), TEXT(3); + private final int value; + + ColumnType(int value) + { + this.value = value; + } + + public int value() + { + return this.value; + } + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excels.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excels.java new file mode 100644 index 0000000..1f1cc81 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Excels.java @@ -0,0 +1,18 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Excel注解集 + * + * @author ruoyi + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Excels +{ + public Excel[] value(); +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Log.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Log.java new file mode 100644 index 0000000..1eb8e49 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Log.java @@ -0,0 +1,51 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.enums.OperatorType; + +/** + * 自定义操作日志记录注解 + * + * @author ruoyi + * + */ +@Target({ ElementType.PARAMETER, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Log +{ + /** + * 模块 + */ + public String title() default ""; + + /** + * 功能 + */ + public BusinessType businessType() default BusinessType.OTHER; + + /** + * 操作人类别 + */ + public OperatorType operatorType() default OperatorType.MANAGE; + + /** + * 是否保存请求的参数 + */ + public boolean isSaveRequestData() default true; + + /** + * 是否保存响应的参数 + */ + public boolean isSaveResponseData() default true; + + /** + * 排除指定的请求参数 + */ + public String[] excludeParamNames() default {}; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RateLimiter.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RateLimiter.java new file mode 100644 index 0000000..0f024c7 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RateLimiter.java @@ -0,0 +1,40 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.enums.LimitType; + +/** + * 限流注解 + * + * @author ruoyi + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RateLimiter +{ + /** + * 限流key + */ + public String key() default CacheConstants.RATE_LIMIT_KEY; + + /** + * 限流时间,单位秒 + */ + public int time() default 60; + + /** + * 限流次数 + */ + public int count() default 100; + + /** + * 限流类型 + */ + public LimitType limitType() default LimitType.DEFAULT; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RepeatSubmit.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RepeatSubmit.java new file mode 100644 index 0000000..b769748 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/RepeatSubmit.java @@ -0,0 +1,31 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 自定义注解防止表单重复提交 + * + * @author ruoyi + * + */ +@Inherited +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RepeatSubmit +{ + /** + * 间隔时间(ms),小于此时间视为重复提交 + */ + public int interval() default 5000; + + /** + * 提示消息 + */ + public String message() default "不允许重复提交,请稍候再试"; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Sensitive.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Sensitive.java new file mode 100644 index 0000000..c0621e9 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/Sensitive.java @@ -0,0 +1,24 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.ruoyi.common.config.serializer.SensitiveJsonSerializer; +import com.ruoyi.common.enums.DesensitizedType; + +/** + * 数据脱敏注解 + * + * @author ruoyi + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@JacksonAnnotationsInside +@JsonSerialize(using = SensitiveJsonSerializer.class) +public @interface Sensitive +{ + DesensitizedType desensitizedType(); +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java b/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java new file mode 100644 index 0000000..29281cf --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java @@ -0,0 +1,122 @@ +package com.ruoyi.common.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 读取项目相关配置 + * + * @author ruoyi + */ +@Component +@ConfigurationProperties(prefix = "ruoyi") +public class RuoYiConfig +{ + /** 项目名称 */ + private String name; + + /** 版本 */ + private String version; + + /** 版权年份 */ + private String copyrightYear; + + /** 上传路径 */ + private static String profile; + + /** 获取地址开关 */ + private static boolean addressEnabled; + + /** 验证码类型 */ + private static String captchaType; + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getVersion() + { + return version; + } + + public void setVersion(String version) + { + this.version = version; + } + + public String getCopyrightYear() + { + return copyrightYear; + } + + public void setCopyrightYear(String copyrightYear) + { + this.copyrightYear = copyrightYear; + } + + public static String getProfile() + { + return profile; + } + + public void setProfile(String profile) + { + RuoYiConfig.profile = profile; + } + + public static boolean isAddressEnabled() + { + return addressEnabled; + } + + public void setAddressEnabled(boolean addressEnabled) + { + RuoYiConfig.addressEnabled = addressEnabled; + } + + public static String getCaptchaType() { + return captchaType; + } + + public void setCaptchaType(String captchaType) { + RuoYiConfig.captchaType = captchaType; + } + + /** + * 获取导入上传路径 + */ + public static String getImportPath() + { + return getProfile() + "/import"; + } + + /** + * 获取头像上传路径 + */ + public static String getAvatarPath() + { + return getProfile() + "/avatar"; + } + + /** + * 获取下载路径 + */ + public static String getDownloadPath() + { + return getProfile() + "/download/"; + } + + /** + * 获取上传路径 + */ + public static String getUploadPath() + { + return getProfile() + "/upload"; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/config/serializer/SensitiveJsonSerializer.java b/ruoyi-common/src/main/java/com/ruoyi/common/config/serializer/SensitiveJsonSerializer.java new file mode 100644 index 0000000..e819a1d --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/config/serializer/SensitiveJsonSerializer.java @@ -0,0 +1,67 @@ +package com.ruoyi.common.config.serializer; + +import java.io.IOException; +import java.util.Objects; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.ContextualSerializer; +import com.ruoyi.common.annotation.Sensitive; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.enums.DesensitizedType; +import com.ruoyi.common.utils.SecurityUtils; + +/** + * 数据脱敏序列化过滤 + * + * @author ruoyi + */ +public class SensitiveJsonSerializer extends JsonSerializer implements ContextualSerializer +{ + private DesensitizedType desensitizedType; + + @Override + public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException + { + if (desensitization()) + { + gen.writeString(desensitizedType.desensitizer().apply(value)); + } + else + { + gen.writeString(value); + } + } + + @Override + public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) + throws JsonMappingException + { + Sensitive annotation = property.getAnnotation(Sensitive.class); + if (Objects.nonNull(annotation) && Objects.equals(String.class, property.getType().getRawClass())) + { + this.desensitizedType = annotation.desensitizedType(); + return this; + } + return prov.findValueSerializer(property.getType(), property); + } + + /** + * 是否需要脱敏处理 + */ + private boolean desensitization() + { + try + { + LoginUser securityUser = SecurityUtils.getLoginUser(); + // 管理员不脱敏 + return !securityUser.getUser().isAdmin(); + } + catch (Exception e) + { + return true; + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java new file mode 100644 index 0000000..0080343 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java @@ -0,0 +1,44 @@ +package com.ruoyi.common.constant; + +/** + * 缓存的key 常量 + * + * @author ruoyi + */ +public class CacheConstants +{ + /** + * 登录用户 redis key + */ + public static final String LOGIN_TOKEN_KEY = "login_tokens:"; + + /** + * 验证码 redis key + */ + public static final String CAPTCHA_CODE_KEY = "captcha_codes:"; + + /** + * 参数管理 cache key + */ + public static final String SYS_CONFIG_KEY = "sys_config:"; + + /** + * 字典管理 cache key + */ + public static final String SYS_DICT_KEY = "sys_dict:"; + + /** + * 防重提交 redis key + */ + public static final String REPEAT_SUBMIT_KEY = "repeat_submit:"; + + /** + * 限流 redis key + */ + public static final String RATE_LIMIT_KEY = "rate_limit:"; + + /** + * 登录账户密码错误次数 redis key + */ + public static final String PWD_ERR_CNT_KEY = "pwd_err_cnt:"; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java new file mode 100644 index 0000000..0c384c6 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java @@ -0,0 +1,173 @@ +package com.ruoyi.common.constant; + +import java.util.Locale; +import io.jsonwebtoken.Claims; + +/** + * 通用常量信息 + * + * @author ruoyi + */ +public class Constants +{ + /** + * UTF-8 字符集 + */ + public static final String UTF8 = "UTF-8"; + + /** + * GBK 字符集 + */ + public static final String GBK = "GBK"; + + /** + * 系统语言 + */ + public static final Locale DEFAULT_LOCALE = Locale.SIMPLIFIED_CHINESE; + + /** + * www主域 + */ + public static final String WWW = "www."; + + /** + * http请求 + */ + public static final String HTTP = "http://"; + + /** + * https请求 + */ + public static final String HTTPS = "https://"; + + /** + * 通用成功标识 + */ + public static final String SUCCESS = "0"; + + /** + * 通用失败标识 + */ + public static final String FAIL = "1"; + + /** + * 登录成功 + */ + public static final String LOGIN_SUCCESS = "Success"; + + /** + * 注销 + */ + public static final String LOGOUT = "Logout"; + + /** + * 注册 + */ + public static final String REGISTER = "Register"; + + /** + * 登录失败 + */ + public static final String LOGIN_FAIL = "Error"; + + /** + * 所有权限标识 + */ + public static final String ALL_PERMISSION = "*:*:*"; + + /** + * 管理员角色权限标识 + */ + public static final String SUPER_ADMIN = "admin"; + + /** + * 角色权限分隔符 + */ + public static final String ROLE_DELIMETER = ","; + + /** + * 权限标识分隔符 + */ + public static final String PERMISSION_DELIMETER = ","; + + /** + * 验证码有效期(分钟) + */ + public static final Integer CAPTCHA_EXPIRATION = 2; + + /** + * 令牌 + */ + public static final String TOKEN = "token"; + + /** + * 令牌前缀 + */ + public static final String TOKEN_PREFIX = "Bearer "; + + /** + * 令牌前缀 + */ + public static final String LOGIN_USER_KEY = "login_user_key"; + + /** + * 用户ID + */ + public static final String JWT_USERID = "userid"; + + /** + * 用户名称 + */ + public static final String JWT_USERNAME = Claims.SUBJECT; + + /** + * 用户头像 + */ + public static final String JWT_AVATAR = "avatar"; + + /** + * 创建时间 + */ + public static final String JWT_CREATED = "created"; + + /** + * 用户权限 + */ + public static final String JWT_AUTHORITIES = "authorities"; + + /** + * 资源映射路径 前缀 + */ + public static final String RESOURCE_PREFIX = "/profile"; + + /** + * RMI 远程方法调用 + */ + public static final String LOOKUP_RMI = "rmi:"; + + /** + * LDAP 远程方法调用 + */ + public static final String LOOKUP_LDAP = "ldap:"; + + /** + * LDAPS 远程方法调用 + */ + public static final String LOOKUP_LDAPS = "ldaps:"; + + /** + * 自动识别json对象白名单配置(仅允许解析的包名,范围越小越安全) + */ + public static final String[] JSON_WHITELIST_STR = { "org.springframework", "com.ruoyi" }; + + /** + * 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加) + */ + public static final String[] JOB_WHITELIST_STR = { "com.ruoyi.quartz.task" }; + + /** + * 定时任务违规的字符 + */ + public static final String[] JOB_ERROR_STR = { "java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml", + "org.springframework", "org.apache", "com.ruoyi.common.utils.file", "com.ruoyi.common.config", "com.ruoyi.generator" }; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/GenConstants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/GenConstants.java new file mode 100644 index 0000000..7d899d4 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/GenConstants.java @@ -0,0 +1,117 @@ +package com.ruoyi.common.constant; + +/** + * 代码生成通用常量 + * + * @author ruoyi + */ +public class GenConstants +{ + /** 单表(增删改查) */ + public static final String TPL_CRUD = "crud"; + + /** 树表(增删改查) */ + public static final String TPL_TREE = "tree"; + + /** 主子表(增删改查) */ + public static final String TPL_SUB = "sub"; + + /** 树编码字段 */ + public static final String TREE_CODE = "treeCode"; + + /** 树父编码字段 */ + public static final String TREE_PARENT_CODE = "treeParentCode"; + + /** 树名称字段 */ + public static final String TREE_NAME = "treeName"; + + /** 上级菜单ID字段 */ + public static final String PARENT_MENU_ID = "parentMenuId"; + + /** 上级菜单名称字段 */ + public static final String PARENT_MENU_NAME = "parentMenuName"; + + /** 数据库字符串类型 */ + public static final String[] COLUMNTYPE_STR = { "char", "varchar", "nvarchar", "varchar2" }; + + /** 数据库文本类型 */ + public static final String[] COLUMNTYPE_TEXT = { "tinytext", "text", "mediumtext", "longtext" }; + + /** 数据库时间类型 */ + public static final String[] COLUMNTYPE_TIME = { "datetime", "time", "date", "timestamp" }; + + /** 数据库数字类型 */ + public static final String[] COLUMNTYPE_NUMBER = { "tinyint", "smallint", "mediumint", "int", "number", "integer", + "bit", "bigint", "float", "double", "decimal" }; + + /** 页面不需要编辑字段 */ + public static final String[] COLUMNNAME_NOT_EDIT = { "id", "create_by", "create_time", "del_flag" }; + + /** 页面不需要显示的列表字段 */ + public static final String[] COLUMNNAME_NOT_LIST = { "id", "create_by", "create_time", "del_flag", "update_by", + "update_time" }; + + /** 页面不需要查询字段 */ + public static final String[] COLUMNNAME_NOT_QUERY = { "id", "create_by", "create_time", "del_flag", "update_by", + "update_time", "remark" }; + + /** Entity基类字段 */ + public static final String[] BASE_ENTITY = { "createBy", "createTime", "updateBy", "updateTime", "remark" }; + + /** Tree基类字段 */ + public static final String[] TREE_ENTITY = { "parentName", "parentId", "orderNum", "ancestors", "children" }; + + /** 文本框 */ + public static final String HTML_INPUT = "input"; + + /** 文本域 */ + public static final String HTML_TEXTAREA = "textarea"; + + /** 下拉框 */ + public static final String HTML_SELECT = "select"; + + /** 单选框 */ + public static final String HTML_RADIO = "radio"; + + /** 复选框 */ + public static final String HTML_CHECKBOX = "checkbox"; + + /** 日期控件 */ + public static final String HTML_DATETIME = "datetime"; + + /** 图片上传控件 */ + public static final String HTML_IMAGE_UPLOAD = "imageUpload"; + + /** 文件上传控件 */ + public static final String HTML_FILE_UPLOAD = "fileUpload"; + + /** 富文本控件 */ + public static final String HTML_EDITOR = "editor"; + + /** 字符串类型 */ + public static final String TYPE_STRING = "String"; + + /** 整型 */ + public static final String TYPE_INTEGER = "Integer"; + + /** 长整型 */ + public static final String TYPE_LONG = "Long"; + + /** 浮点型 */ + public static final String TYPE_DOUBLE = "Double"; + + /** 高精度计算类型 */ + public static final String TYPE_BIGDECIMAL = "BigDecimal"; + + /** 时间类型 */ + public static final String TYPE_DATE = "Date"; + + /** 模糊查询 */ + public static final String QUERY_LIKE = "LIKE"; + + /** 相等查询 */ + public static final String QUERY_EQ = "EQ"; + + /** 需要 */ + public static final String REQUIRE = "1"; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/HttpStatus.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/HttpStatus.java new file mode 100644 index 0000000..a983c77 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/HttpStatus.java @@ -0,0 +1,94 @@ +package com.ruoyi.common.constant; + +/** + * 返回状态码 + * + * @author ruoyi + */ +public class HttpStatus +{ + /** + * 操作成功 + */ + public static final int SUCCESS = 200; + + /** + * 对象创建成功 + */ + public static final int CREATED = 201; + + /** + * 请求已经被接受 + */ + public static final int ACCEPTED = 202; + + /** + * 操作已经执行成功,但是没有返回数据 + */ + public static final int NO_CONTENT = 204; + + /** + * 资源已被移除 + */ + public static final int MOVED_PERM = 301; + + /** + * 重定向 + */ + public static final int SEE_OTHER = 303; + + /** + * 资源没有被修改 + */ + public static final int NOT_MODIFIED = 304; + + /** + * 参数列表错误(缺少,格式不匹配) + */ + public static final int BAD_REQUEST = 400; + + /** + * 未授权 + */ + public static final int UNAUTHORIZED = 401; + + /** + * 访问受限,授权过期 + */ + public static final int FORBIDDEN = 403; + + /** + * 资源,服务未找到 + */ + public static final int NOT_FOUND = 404; + + /** + * 不允许的http方法 + */ + public static final int BAD_METHOD = 405; + + /** + * 资源冲突,或者资源被锁 + */ + public static final int CONFLICT = 409; + + /** + * 不支持的数据,媒体类型 + */ + public static final int UNSUPPORTED_TYPE = 415; + + /** + * 系统内部错误 + */ + public static final int ERROR = 500; + + /** + * 接口未实现 + */ + public static final int NOT_IMPLEMENTED = 501; + + /** + * 系统警告消息 + */ + public static final int WARN = 601; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/ScheduleConstants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/ScheduleConstants.java new file mode 100644 index 0000000..62ad815 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/ScheduleConstants.java @@ -0,0 +1,50 @@ +package com.ruoyi.common.constant; + +/** + * 任务调度通用常量 + * + * @author ruoyi + */ +public class ScheduleConstants +{ + public static final String TASK_CLASS_NAME = "TASK_CLASS_NAME"; + + /** 执行目标key */ + public static final String TASK_PROPERTIES = "TASK_PROPERTIES"; + + /** 默认 */ + public static final String MISFIRE_DEFAULT = "0"; + + /** 立即触发执行 */ + public static final String MISFIRE_IGNORE_MISFIRES = "1"; + + /** 触发一次执行 */ + public static final String MISFIRE_FIRE_AND_PROCEED = "2"; + + /** 不触发立即执行 */ + public static final String MISFIRE_DO_NOTHING = "3"; + + public enum Status + { + /** + * 正常 + */ + NORMAL("0"), + /** + * 暂停 + */ + PAUSE("1"); + + private String value; + + private Status(String value) + { + this.value = value; + } + + public String getValue() + { + return value; + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java new file mode 100644 index 0000000..8dc7faa --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java @@ -0,0 +1,81 @@ +package com.ruoyi.common.constant; + +/** + * 用户常量信息 + * + * @author ruoyi + */ +public class UserConstants +{ + /** + * 平台内系统用户的唯一标志 + */ + public static final String SYS_USER = "SYS_USER"; + + /** 正常状态 */ + public static final String NORMAL = "0"; + + /** 异常状态 */ + public static final String EXCEPTION = "1"; + + /** 用户封禁状态 */ + public static final String USER_DISABLE = "1"; + + /** 角色正常状态 */ + public static final String ROLE_NORMAL = "0"; + + /** 角色封禁状态 */ + public static final String ROLE_DISABLE = "1"; + + /** 部门正常状态 */ + public static final String DEPT_NORMAL = "0"; + + /** 部门停用状态 */ + public static final String DEPT_DISABLE = "1"; + + /** 字典正常状态 */ + public static final String DICT_NORMAL = "0"; + + /** 是否为系统默认(是) */ + public static final String YES = "Y"; + + /** 是否菜单外链(是) */ + public static final String YES_FRAME = "0"; + + /** 是否菜单外链(否) */ + public static final String NO_FRAME = "1"; + + /** 菜单类型(目录) */ + public static final String TYPE_DIR = "M"; + + /** 菜单类型(菜单) */ + public static final String TYPE_MENU = "C"; + + /** 菜单类型(按钮) */ + public static final String TYPE_BUTTON = "F"; + + /** Layout组件标识 */ + public final static String LAYOUT = "Layout"; + + /** ParentView组件标识 */ + public final static String PARENT_VIEW = "ParentView"; + + /** InnerLink组件标识 */ + public final static String INNER_LINK = "InnerLink"; + + /** 校验是否唯一的返回标识 */ + public final static boolean UNIQUE = true; + public final static boolean NOT_UNIQUE = false; + + /** + * 用户名长度限制 + */ + public static final int USERNAME_MIN_LENGTH = 2; + public static final int USERNAME_MAX_LENGTH = 20; + + /** + * 密码长度限制 + */ + public static final int PASSWORD_MIN_LENGTH = 5; + public static final int PASSWORD_MAX_LENGTH = 20; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/controller/BaseController.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/controller/BaseController.java new file mode 100644 index 0000000..a685e06 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/controller/BaseController.java @@ -0,0 +1,202 @@ +package com.ruoyi.common.core.controller; + +import java.beans.PropertyEditorSupport; +import java.util.Date; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.page.PageDomain; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.core.page.TableSupport; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.PageUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.sql.SqlUtil; + +/** + * web层通用数据处理 + * + * @author ruoyi + */ +public class BaseController +{ + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + + /** + * 将前台传递过来的日期格式的字符串,自动转化为Date类型 + */ + @InitBinder + public void initBinder(WebDataBinder binder) + { + // Date 类型转换 + binder.registerCustomEditor(Date.class, new PropertyEditorSupport() + { + @Override + public void setAsText(String text) + { + setValue(DateUtils.parseDate(text)); + } + }); + } + + /** + * 设置请求分页数据 + */ + protected void startPage() + { + PageUtils.startPage(); + } + + /** + * 设置请求排序数据 + */ + protected void startOrderBy() + { + PageDomain pageDomain = TableSupport.buildPageRequest(); + if (StringUtils.isNotEmpty(pageDomain.getOrderBy())) + { + String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy()); + PageHelper.orderBy(orderBy); + } + } + + /** + * 清理分页的线程变量 + */ + protected void clearPage() + { + PageUtils.clearPage(); + } + + /** + * 响应请求分页数据 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected TableDataInfo getDataTable(List list) + { + TableDataInfo rspData = new TableDataInfo(); + rspData.setCode(HttpStatus.SUCCESS); + rspData.setMsg("查询成功"); + rspData.setRows(list); + rspData.setTotal(new PageInfo(list).getTotal()); + return rspData; + } + + /** + * 返回成功 + */ + public AjaxResult success() + { + return AjaxResult.success(); + } + + /** + * 返回失败消息 + */ + public AjaxResult error() + { + return AjaxResult.error(); + } + + /** + * 返回成功消息 + */ + public AjaxResult success(String message) + { + return AjaxResult.success(message); + } + + /** + * 返回成功消息 + */ + public AjaxResult success(Object data) + { + return AjaxResult.success(data); + } + + /** + * 返回失败消息 + */ + public AjaxResult error(String message) + { + return AjaxResult.error(message); + } + + /** + * 返回警告消息 + */ + public AjaxResult warn(String message) + { + return AjaxResult.warn(message); + } + + /** + * 响应返回结果 + * + * @param rows 影响行数 + * @return 操作结果 + */ + protected AjaxResult toAjax(int rows) + { + return rows > 0 ? AjaxResult.success() : AjaxResult.error(); + } + + /** + * 响应返回结果 + * + * @param result 结果 + * @return 操作结果 + */ + protected AjaxResult toAjax(boolean result) + { + return result ? success() : error(); + } + + /** + * 页面跳转 + */ + public String redirect(String url) + { + return StringUtils.format("redirect:{}", url); + } + + /** + * 获取用户缓存信息 + */ + public LoginUser getLoginUser() + { + return SecurityUtils.getLoginUser(); + } + + /** + * 获取登录用户id + */ + public Long getUserId() + { + return getLoginUser().getUserId(); + } + + /** + * 获取登录部门id + */ + public Long getDeptId() + { + return getLoginUser().getDeptId(); + } + + /** + * 获取登录用户名 + */ + public String getUsername() + { + return getLoginUser().getUsername(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/AjaxResult.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/AjaxResult.java new file mode 100644 index 0000000..a7abfe4 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/AjaxResult.java @@ -0,0 +1,216 @@ +package com.ruoyi.common.core.domain; + +import java.util.HashMap; +import java.util.Objects; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.utils.StringUtils; + +/** + * 操作消息提醒 + * + * @author ruoyi + */ +public class AjaxResult extends HashMap +{ + private static final long serialVersionUID = 1L; + + /** 状态码 */ + public static final String CODE_TAG = "code"; + + /** 返回内容 */ + public static final String MSG_TAG = "msg"; + + /** 数据对象 */ + public static final String DATA_TAG = "data"; + + /** + * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。 + */ + public AjaxResult() + { + } + + /** + * 初始化一个新创建的 AjaxResult 对象 + * + * @param code 状态码 + * @param msg 返回内容 + */ + public AjaxResult(int code, String msg) + { + super.put(CODE_TAG, code); + super.put(MSG_TAG, msg); + } + + /** + * 初始化一个新创建的 AjaxResult 对象 + * + * @param code 状态码 + * @param msg 返回内容 + * @param data 数据对象 + */ + public AjaxResult(int code, String msg, Object data) + { + super.put(CODE_TAG, code); + super.put(MSG_TAG, msg); + if (StringUtils.isNotNull(data)) + { + super.put(DATA_TAG, data); + } + } + + /** + * 返回成功消息 + * + * @return 成功消息 + */ + public static AjaxResult success() + { + return AjaxResult.success("操作成功"); + } + + /** + * 返回成功数据 + * + * @return 成功消息 + */ + public static AjaxResult success(Object data) + { + return AjaxResult.success("操作成功", data); + } + + /** + * 返回成功消息 + * + * @param msg 返回内容 + * @return 成功消息 + */ + public static AjaxResult success(String msg) + { + return AjaxResult.success(msg, null); + } + + /** + * 返回成功消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 成功消息 + */ + public static AjaxResult success(String msg, Object data) + { + return new AjaxResult(HttpStatus.SUCCESS, msg, data); + } + + /** + * 返回警告消息 + * + * @param msg 返回内容 + * @return 警告消息 + */ + public static AjaxResult warn(String msg) + { + return AjaxResult.warn(msg, null); + } + + /** + * 返回警告消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 警告消息 + */ + public static AjaxResult warn(String msg, Object data) + { + return new AjaxResult(HttpStatus.WARN, msg, data); + } + + /** + * 返回错误消息 + * + * @return 错误消息 + */ + public static AjaxResult error() + { + return AjaxResult.error("操作失败"); + } + + /** + * 返回错误消息 + * + * @param msg 返回内容 + * @return 错误消息 + */ + public static AjaxResult error(String msg) + { + return AjaxResult.error(msg, null); + } + + /** + * 返回错误消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 错误消息 + */ + public static AjaxResult error(String msg, Object data) + { + return new AjaxResult(HttpStatus.ERROR, msg, data); + } + + /** + * 返回错误消息 + * + * @param code 状态码 + * @param msg 返回内容 + * @return 错误消息 + */ + public static AjaxResult error(int code, String msg) + { + return new AjaxResult(code, msg, null); + } + + /** + * 是否为成功消息 + * + * @return 结果 + */ + public boolean isSuccess() + { + return Objects.equals(HttpStatus.SUCCESS, this.get(CODE_TAG)); + } + + /** + * 是否为警告消息 + * + * @return 结果 + */ + public boolean isWarn() + { + return Objects.equals(HttpStatus.WARN, this.get(CODE_TAG)); + } + + /** + * 是否为错误消息 + * + * @return 结果 + */ + public boolean isError() + { + return Objects.equals(HttpStatus.ERROR, this.get(CODE_TAG)); + } + + /** + * 方便链式调用 + * + * @param key 键 + * @param value 值 + * @return 数据对象 + */ + @Override + public AjaxResult put(String key, Object value) + { + super.put(key, value); + return this; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java new file mode 100644 index 0000000..15bf66b --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java @@ -0,0 +1,118 @@ +package com.ruoyi.common.core.domain; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Entity基类 + * + * @author ruoyi + */ +public class BaseEntity implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 搜索值 */ + @JsonIgnore + private String searchValue; + + /** 创建者 */ + private String createBy; + + /** 创建时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** 更新者 */ + private String updateBy; + + /** 更新时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** 备注 */ + private String remark; + + /** 请求参数 */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Map params; + + public String getSearchValue() + { + return searchValue; + } + + public void setSearchValue(String searchValue) + { + this.searchValue = searchValue; + } + + public String getCreateBy() + { + return createBy; + } + + public void setCreateBy(String createBy) + { + this.createBy = createBy; + } + + public Date getCreateTime() + { + return createTime; + } + + public void setCreateTime(Date createTime) + { + this.createTime = createTime; + } + + public String getUpdateBy() + { + return updateBy; + } + + public void setUpdateBy(String updateBy) + { + this.updateBy = updateBy; + } + + public Date getUpdateTime() + { + return updateTime; + } + + public void setUpdateTime(Date updateTime) + { + this.updateTime = updateTime; + } + + public String getRemark() + { + return remark; + } + + public void setRemark(String remark) + { + this.remark = remark; + } + + public Map getParams() + { + if (params == null) + { + params = new HashMap<>(); + } + return params; + } + + public void setParams(Map params) + { + this.params = params; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/R.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/R.java new file mode 100644 index 0000000..ef15802 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/R.java @@ -0,0 +1,115 @@ +package com.ruoyi.common.core.domain; + +import java.io.Serializable; +import com.ruoyi.common.constant.HttpStatus; + +/** + * 响应信息主体 + * + * @author ruoyi + */ +public class R implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 成功 */ + public static final int SUCCESS = HttpStatus.SUCCESS; + + /** 失败 */ + public static final int FAIL = HttpStatus.ERROR; + + private int code; + + private String msg; + + private T data; + + public static R ok() + { + return restResult(null, SUCCESS, "操作成功"); + } + + public static R ok(T data) + { + return restResult(data, SUCCESS, "操作成功"); + } + + public static R ok(T data, String msg) + { + return restResult(data, SUCCESS, msg); + } + + public static R fail() + { + return restResult(null, FAIL, "操作失败"); + } + + public static R fail(String msg) + { + return restResult(null, FAIL, msg); + } + + public static R fail(T data) + { + return restResult(data, FAIL, "操作失败"); + } + + public static R fail(T data, String msg) + { + return restResult(data, FAIL, msg); + } + + public static R fail(int code, String msg) + { + return restResult(null, code, msg); + } + + private static R restResult(T data, int code, String msg) + { + R apiResult = new R<>(); + apiResult.setCode(code); + apiResult.setData(data); + apiResult.setMsg(msg); + return apiResult; + } + + public int getCode() + { + return code; + } + + public void setCode(int code) + { + this.code = code; + } + + public String getMsg() + { + return msg; + } + + public void setMsg(String msg) + { + this.msg = msg; + } + + public T getData() + { + return data; + } + + public void setData(T data) + { + this.data = data; + } + + public static Boolean isError(R ret) + { + return !isSuccess(ret); + } + + public static Boolean isSuccess(R ret) + { + return R.SUCCESS == ret.getCode(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeEntity.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeEntity.java new file mode 100644 index 0000000..a180a18 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeEntity.java @@ -0,0 +1,79 @@ +package com.ruoyi.common.core.domain; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tree基类 + * + * @author ruoyi + */ +public class TreeEntity extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 父菜单名称 */ + private String parentName; + + /** 父菜单ID */ + private Long parentId; + + /** 显示顺序 */ + private Integer orderNum; + + /** 祖级列表 */ + private String ancestors; + + /** 子部门 */ + private List children = new ArrayList<>(); + + public String getParentName() + { + return parentName; + } + + public void setParentName(String parentName) + { + this.parentName = parentName; + } + + public Long getParentId() + { + return parentId; + } + + public void setParentId(Long parentId) + { + this.parentId = parentId; + } + + public Integer getOrderNum() + { + return orderNum; + } + + public void setOrderNum(Integer orderNum) + { + this.orderNum = orderNum; + } + + public String getAncestors() + { + return ancestors; + } + + public void setAncestors(String ancestors) + { + this.ancestors = ancestors; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeSelect.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeSelect.java new file mode 100644 index 0000000..ae25df2 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/TreeSelect.java @@ -0,0 +1,93 @@ +package com.ruoyi.common.core.domain; + +import java.io.Serializable; +import java.util.List; +import java.util.stream.Collectors; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysDept; +import com.ruoyi.common.core.domain.entity.SysMenu; +import com.ruoyi.common.utils.StringUtils; + +/** + * Treeselect树结构实体类 + * + * @author ruoyi + */ +public class TreeSelect implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 节点ID */ + private Long id; + + /** 节点名称 */ + private String label; + + /** 节点禁用 */ + private boolean disabled = false; + + /** 子节点 */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private List children; + + public TreeSelect() + { + + } + + public TreeSelect(SysDept dept) + { + this.id = dept.getDeptId(); + this.label = dept.getDeptName(); + this.disabled = StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()); + this.children = dept.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + public TreeSelect(SysMenu menu) + { + this.id = menu.getMenuId(); + this.label = menu.getMenuName(); + this.children = menu.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + public Long getId() + { + return id; + } + + public void setId(Long id) + { + this.id = id; + } + + public String getLabel() + { + return label; + } + + public void setLabel(String label) + { + this.label = label; + } + + public boolean isDisabled() + { + return disabled; + } + + public void setDisabled(boolean disabled) + { + this.disabled = disabled; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDept.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDept.java new file mode 100644 index 0000000..fb18c5c --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDept.java @@ -0,0 +1,203 @@ +package com.ruoyi.common.core.domain.entity; + +import java.util.ArrayList; +import java.util.List; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 部门表 sys_dept + * + * @author ruoyi + */ +public class SysDept extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 部门ID */ + private Long deptId; + + /** 父部门ID */ + private Long parentId; + + /** 祖级列表 */ + private String ancestors; + + /** 部门名称 */ + private String deptName; + + /** 显示顺序 */ + private Integer orderNum; + + /** 负责人 */ + private String leader; + + /** 联系电话 */ + private String phone; + + /** 邮箱 */ + private String email; + + /** 部门状态:0正常,1停用 */ + private String status; + + /** 删除标志(0代表存在 2代表删除) */ + private String delFlag; + + /** 父部门名称 */ + private String parentName; + + /** 子部门 */ + private List children = new ArrayList(); + + public Long getDeptId() + { + return deptId; + } + + public void setDeptId(Long deptId) + { + this.deptId = deptId; + } + + public Long getParentId() + { + return parentId; + } + + public void setParentId(Long parentId) + { + this.parentId = parentId; + } + + public String getAncestors() + { + return ancestors; + } + + public void setAncestors(String ancestors) + { + this.ancestors = ancestors; + } + + @NotBlank(message = "部门名称不能为空") + @Size(min = 0, max = 30, message = "部门名称长度不能超过30个字符") + public String getDeptName() + { + return deptName; + } + + public void setDeptName(String deptName) + { + this.deptName = deptName; + } + + @NotNull(message = "显示顺序不能为空") + public Integer getOrderNum() + { + return orderNum; + } + + public void setOrderNum(Integer orderNum) + { + this.orderNum = orderNum; + } + + public String getLeader() + { + return leader; + } + + public void setLeader(String leader) + { + this.leader = leader; + } + + @Size(min = 0, max = 11, message = "联系电话长度不能超过11个字符") + public String getPhone() + { + return phone; + } + + public void setPhone(String phone) + { + this.phone = phone; + } + + @Email(message = "邮箱格式不正确") + @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符") + public String getEmail() + { + return email; + } + + public void setEmail(String email) + { + this.email = email; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getDelFlag() + { + return delFlag; + } + + public void setDelFlag(String delFlag) + { + this.delFlag = delFlag; + } + + public String getParentName() + { + return parentName; + } + + public void setParentName(String parentName) + { + this.parentName = parentName; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("deptId", getDeptId()) + .append("parentId", getParentId()) + .append("ancestors", getAncestors()) + .append("deptName", getDeptName()) + .append("orderNum", getOrderNum()) + .append("leader", getLeader()) + .append("phone", getPhone()) + .append("email", getEmail()) + .append("status", getStatus()) + .append("delFlag", getDelFlag()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .toString(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictData.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictData.java new file mode 100644 index 0000000..738f12c --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictData.java @@ -0,0 +1,176 @@ +package com.ruoyi.common.core.domain.entity; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 字典数据表 sys_dict_data + * + * @author ruoyi + */ +public class SysDictData extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 字典编码 */ + @Excel(name = "字典编码", cellType = ColumnType.NUMERIC) + private Long dictCode; + + /** 字典排序 */ + @Excel(name = "字典排序", cellType = ColumnType.NUMERIC) + private Long dictSort; + + /** 字典标签 */ + @Excel(name = "字典标签") + private String dictLabel; + + /** 字典键值 */ + @Excel(name = "字典键值") + private String dictValue; + + /** 字典类型 */ + @Excel(name = "字典类型") + private String dictType; + + /** 样式属性(其他样式扩展) */ + private String cssClass; + + /** 表格字典样式 */ + private String listClass; + + /** 是否默认(Y是 N否) */ + @Excel(name = "是否默认", readConverterExp = "Y=是,N=否") + private String isDefault; + + /** 状态(0正常 1停用) */ + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + public Long getDictCode() + { + return dictCode; + } + + public void setDictCode(Long dictCode) + { + this.dictCode = dictCode; + } + + public Long getDictSort() + { + return dictSort; + } + + public void setDictSort(Long dictSort) + { + this.dictSort = dictSort; + } + + @NotBlank(message = "字典标签不能为空") + @Size(min = 0, max = 100, message = "字典标签长度不能超过100个字符") + public String getDictLabel() + { + return dictLabel; + } + + public void setDictLabel(String dictLabel) + { + this.dictLabel = dictLabel; + } + + @NotBlank(message = "字典键值不能为空") + @Size(min = 0, max = 100, message = "字典键值长度不能超过100个字符") + public String getDictValue() + { + return dictValue; + } + + public void setDictValue(String dictValue) + { + this.dictValue = dictValue; + } + + @NotBlank(message = "字典类型不能为空") + @Size(min = 0, max = 100, message = "字典类型长度不能超过100个字符") + public String getDictType() + { + return dictType; + } + + public void setDictType(String dictType) + { + this.dictType = dictType; + } + + @Size(min = 0, max = 100, message = "样式属性长度不能超过100个字符") + public String getCssClass() + { + return cssClass; + } + + public void setCssClass(String cssClass) + { + this.cssClass = cssClass; + } + + public String getListClass() + { + return listClass; + } + + public void setListClass(String listClass) + { + this.listClass = listClass; + } + + public boolean getDefault() + { + return UserConstants.YES.equals(this.isDefault); + } + + public String getIsDefault() + { + return isDefault; + } + + public void setIsDefault(String isDefault) + { + this.isDefault = isDefault; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("dictCode", getDictCode()) + .append("dictSort", getDictSort()) + .append("dictLabel", getDictLabel()) + .append("dictValue", getDictValue()) + .append("dictType", getDictType()) + .append("cssClass", getCssClass()) + .append("listClass", getListClass()) + .append("isDefault", getIsDefault()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictType.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictType.java new file mode 100644 index 0000000..e324fcf --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysDictType.java @@ -0,0 +1,96 @@ +package com.ruoyi.common.core.domain.entity; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 字典类型表 sys_dict_type + * + * @author ruoyi + */ +public class SysDictType extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 字典主键 */ + @Excel(name = "字典主键", cellType = ColumnType.NUMERIC) + private Long dictId; + + /** 字典名称 */ + @Excel(name = "字典名称") + private String dictName; + + /** 字典类型 */ + @Excel(name = "字典类型") + private String dictType; + + /** 状态(0正常 1停用) */ + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + public Long getDictId() + { + return dictId; + } + + public void setDictId(Long dictId) + { + this.dictId = dictId; + } + + @NotBlank(message = "字典名称不能为空") + @Size(min = 0, max = 100, message = "字典类型名称长度不能超过100个字符") + public String getDictName() + { + return dictName; + } + + public void setDictName(String dictName) + { + this.dictName = dictName; + } + + @NotBlank(message = "字典类型不能为空") + @Size(min = 0, max = 100, message = "字典类型类型长度不能超过100个字符") + @Pattern(regexp = "^[a-z][a-z0-9_]*$", message = "字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)") + public String getDictType() + { + return dictType; + } + + public void setDictType(String dictType) + { + this.dictType = dictType; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("dictId", getDictId()) + .append("dictName", getDictName()) + .append("dictType", getDictType()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysMenu.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysMenu.java new file mode 100644 index 0000000..ad73692 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysMenu.java @@ -0,0 +1,274 @@ +package com.ruoyi.common.core.domain.entity; + +import java.util.ArrayList; +import java.util.List; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 菜单权限表 sys_menu + * + * @author ruoyi + */ +public class SysMenu extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 菜单ID */ + private Long menuId; + + /** 菜单名称 */ + private String menuName; + + /** 父菜单名称 */ + private String parentName; + + /** 父菜单ID */ + private Long parentId; + + /** 显示顺序 */ + private Integer orderNum; + + /** 路由地址 */ + private String path; + + /** 组件路径 */ + private String component; + + /** 路由参数 */ + private String query; + + /** 路由名称,默认和路由地址相同的驼峰格式(注意:因为vue3版本的router会删除名称相同路由,为避免名字的冲突,特殊情况可以自定义) */ + private String routeName; + + /** 是否为外链(0是 1否) */ + private String isFrame; + + /** 是否缓存(0缓存 1不缓存) */ + private String isCache; + + /** 类型(M目录 C菜单 F按钮) */ + private String menuType; + + /** 显示状态(0显示 1隐藏) */ + private String visible; + + /** 菜单状态(0正常 1停用) */ + private String status; + + /** 权限字符串 */ + private String perms; + + /** 菜单图标 */ + private String icon; + + /** 子菜单 */ + private List children = new ArrayList(); + + public Long getMenuId() + { + return menuId; + } + + public void setMenuId(Long menuId) + { + this.menuId = menuId; + } + + @NotBlank(message = "菜单名称不能为空") + @Size(min = 0, max = 50, message = "菜单名称长度不能超过50个字符") + public String getMenuName() + { + return menuName; + } + + public void setMenuName(String menuName) + { + this.menuName = menuName; + } + + public String getParentName() + { + return parentName; + } + + public void setParentName(String parentName) + { + this.parentName = parentName; + } + + public Long getParentId() + { + return parentId; + } + + public void setParentId(Long parentId) + { + this.parentId = parentId; + } + + @NotNull(message = "显示顺序不能为空") + public Integer getOrderNum() + { + return orderNum; + } + + public void setOrderNum(Integer orderNum) + { + this.orderNum = orderNum; + } + + @Size(min = 0, max = 200, message = "路由地址不能超过200个字符") + public String getPath() + { + return path; + } + + public void setPath(String path) + { + this.path = path; + } + + @Size(min = 0, max = 200, message = "组件路径不能超过255个字符") + public String getComponent() + { + return component; + } + + public void setComponent(String component) + { + this.component = component; + } + + public String getQuery() + { + return query; + } + + public void setQuery(String query) + { + this.query = query; + } + + public String getRouteName() + { + return routeName; + } + + public void setRouteName(String routeName) + { + this.routeName = routeName; + } + + public String getIsFrame() + { + return isFrame; + } + + public void setIsFrame(String isFrame) + { + this.isFrame = isFrame; + } + + public String getIsCache() + { + return isCache; + } + + public void setIsCache(String isCache) + { + this.isCache = isCache; + } + + @NotBlank(message = "菜单类型不能为空") + public String getMenuType() + { + return menuType; + } + + public void setMenuType(String menuType) + { + this.menuType = menuType; + } + + public String getVisible() + { + return visible; + } + + public void setVisible(String visible) + { + this.visible = visible; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + @Size(min = 0, max = 100, message = "权限标识长度不能超过100个字符") + public String getPerms() + { + return perms; + } + + public void setPerms(String perms) + { + this.perms = perms; + } + + public String getIcon() + { + return icon; + } + + public void setIcon(String icon) + { + this.icon = icon; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("menuId", getMenuId()) + .append("menuName", getMenuName()) + .append("parentId", getParentId()) + .append("orderNum", getOrderNum()) + .append("path", getPath()) + .append("component", getComponent()) + .append("query", getQuery()) + .append("routeName", getRouteName()) + .append("isFrame", getIsFrame()) + .append("IsCache", getIsCache()) + .append("menuType", getMenuType()) + .append("visible", getVisible()) + .append("status ", getStatus()) + .append("perms", getPerms()) + .append("icon", getIcon()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysRole.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysRole.java new file mode 100644 index 0000000..488d49c --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysRole.java @@ -0,0 +1,241 @@ +package com.ruoyi.common.core.domain.entity; + +import java.util.Set; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 角色表 sys_role + * + * @author ruoyi + */ +public class SysRole extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 角色ID */ + @Excel(name = "角色序号", cellType = ColumnType.NUMERIC) + private Long roleId; + + /** 角色名称 */ + @Excel(name = "角色名称") + private String roleName; + + /** 角色权限 */ + @Excel(name = "角色权限") + private String roleKey; + + /** 角色排序 */ + @Excel(name = "角色排序") + private Integer roleSort; + + /** 数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限) */ + @Excel(name = "数据范围", readConverterExp = "1=所有数据权限,2=自定义数据权限,3=本部门数据权限,4=本部门及以下数据权限,5=仅本人数据权限") + private String dataScope; + + /** 菜单树选择项是否关联显示( 0:父子不互相关联显示 1:父子互相关联显示) */ + private boolean menuCheckStrictly; + + /** 部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示 ) */ + private boolean deptCheckStrictly; + + /** 角色状态(0正常 1停用) */ + @Excel(name = "角色状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** 删除标志(0代表存在 2代表删除) */ + private String delFlag; + + /** 用户是否存在此角色标识 默认不存在 */ + private boolean flag = false; + + /** 菜单组 */ + private Long[] menuIds; + + /** 部门组(数据权限) */ + private Long[] deptIds; + + /** 角色菜单权限 */ + private Set permissions; + + public SysRole() + { + + } + + public SysRole(Long roleId) + { + this.roleId = roleId; + } + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + public boolean isAdmin() + { + return isAdmin(this.roleId); + } + + public static boolean isAdmin(Long roleId) + { + return roleId != null && 1L == roleId; + } + + @NotBlank(message = "角色名称不能为空") + @Size(min = 0, max = 30, message = "角色名称长度不能超过30个字符") + public String getRoleName() + { + return roleName; + } + + public void setRoleName(String roleName) + { + this.roleName = roleName; + } + + @NotBlank(message = "权限字符不能为空") + @Size(min = 0, max = 100, message = "权限字符长度不能超过100个字符") + public String getRoleKey() + { + return roleKey; + } + + public void setRoleKey(String roleKey) + { + this.roleKey = roleKey; + } + + @NotNull(message = "显示顺序不能为空") + public Integer getRoleSort() + { + return roleSort; + } + + public void setRoleSort(Integer roleSort) + { + this.roleSort = roleSort; + } + + public String getDataScope() + { + return dataScope; + } + + public void setDataScope(String dataScope) + { + this.dataScope = dataScope; + } + + public boolean isMenuCheckStrictly() + { + return menuCheckStrictly; + } + + public void setMenuCheckStrictly(boolean menuCheckStrictly) + { + this.menuCheckStrictly = menuCheckStrictly; + } + + public boolean isDeptCheckStrictly() + { + return deptCheckStrictly; + } + + public void setDeptCheckStrictly(boolean deptCheckStrictly) + { + this.deptCheckStrictly = deptCheckStrictly; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getDelFlag() + { + return delFlag; + } + + public void setDelFlag(String delFlag) + { + this.delFlag = delFlag; + } + + public boolean isFlag() + { + return flag; + } + + public void setFlag(boolean flag) + { + this.flag = flag; + } + + public Long[] getMenuIds() + { + return menuIds; + } + + public void setMenuIds(Long[] menuIds) + { + this.menuIds = menuIds; + } + + public Long[] getDeptIds() + { + return deptIds; + } + + public void setDeptIds(Long[] deptIds) + { + this.deptIds = deptIds; + } + + public Set getPermissions() + { + return permissions; + } + + public void setPermissions(Set permissions) + { + this.permissions = permissions; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("roleId", getRoleId()) + .append("roleName", getRoleName()) + .append("roleKey", getRoleKey()) + .append("roleSort", getRoleSort()) + .append("dataScope", getDataScope()) + .append("menuCheckStrictly", isMenuCheckStrictly()) + .append("deptCheckStrictly", isDeptCheckStrictly()) + .append("status", getStatus()) + .append("delFlag", getDelFlag()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java new file mode 100644 index 0000000..d133ee8 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java @@ -0,0 +1,338 @@ +package com.ruoyi.common.core.domain.entity; + +import java.util.Date; +import java.util.List; +import javax.validation.constraints.*; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.annotation.Excel.Type; +import com.ruoyi.common.annotation.Excels; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.xss.Xss; + +/** + * 用户对象 sys_user + * + * @author ruoyi + */ +public class SysUser extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 用户ID */ + @Excel(name = "用户序号", type = Type.EXPORT, cellType = ColumnType.NUMERIC, prompt = "用户编号") + private Long userId; + + /** 部门ID */ + @Excel(name = "部门编号", type = Type.IMPORT) + private Long deptId; + + /** 用户账号 */ + @Excel(name = "登录名称") + private String userName; + + /** 用户昵称 */ + @Excel(name = "用户名称") + private String nickName; + + /** 用户邮箱 */ + @Excel(name = "用户邮箱") + private String email; + + /** 手机号码 */ + @Excel(name = "手机号码", cellType = ColumnType.TEXT) + private String phonenumber; + + /** 用户性别 */ + @Excel(name = "用户性别", readConverterExp = "0=男,1=女,2=未知") + private String sex; + + /** 用户头像 */ + private String avatar; + + /** 密码 */ + private String password; + + /** 账号状态(0正常 1停用) */ + @Excel(name = "账号状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** 删除标志(0代表存在 2代表删除) */ + private String delFlag; + + /** 最后登录IP */ + @Excel(name = "最后登录IP", type = Type.EXPORT) + private String loginIp; + + /** 最后登录时间 */ + @Excel(name = "最后登录时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss", type = Type.EXPORT) + private Date loginDate; + + /** 密码最后更新时间 */ + private Date pwdUpdateDate; + + /** 部门对象 */ + @Excels({ + @Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT), + @Excel(name = "部门负责人", targetAttr = "leader", type = Type.EXPORT) + }) + private SysDept dept; + + /** 角色对象 */ + private List roles; + + /** 角色组 */ + private Long[] roleIds; + + /** 岗位组 */ + private Long[] postIds; + + /** 角色ID */ + private Long roleId; + + public SysUser() + { + + } + + public SysUser(Long userId) + { + this.userId = userId; + } + + public Long getUserId() + { + return userId; + } + + public void setUserId(Long userId) + { + this.userId = userId; + } + + public boolean isAdmin() + { + return isAdmin(this.userId); + } + + public static boolean isAdmin(Long userId) + { + return userId != null && 1L == userId; + } + + public Long getDeptId() + { + return deptId; + } + + public void setDeptId(Long deptId) + { + this.deptId = deptId; + } + + @Xss(message = "用户昵称不能包含脚本字符") + @Size(min = 0, max = 30, message = "用户昵称长度不能超过30个字符") + public String getNickName() + { + return nickName; + } + + public void setNickName(String nickName) + { + this.nickName = nickName; + } + + @Xss(message = "用户账号不能包含脚本字符") + @NotBlank(message = "用户账号不能为空") + @Size(min = 0, max = 30, message = "用户账号长度不能超过30个字符") + public String getUserName() + { + return userName; + } + + public void setUserName(String userName) + { + this.userName = userName; + } + + @Email(message = "邮箱格式不正确") + @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符") + public String getEmail() + { + return email; + } + + public void setEmail(String email) + { + this.email = email; + } + + @Size(min = 0, max = 11, message = "手机号码长度不能超过11个字符") + public String getPhonenumber() + { + return phonenumber; + } + + public void setPhonenumber(String phonenumber) + { + this.phonenumber = phonenumber; + } + + public String getSex() + { + return sex; + } + + public void setSex(String sex) + { + this.sex = sex; + } + + public String getAvatar() + { + return avatar; + } + + public void setAvatar(String avatar) + { + this.avatar = avatar; + } + + public String getPassword() + { + return password; + } + + public void setPassword(String password) + { + this.password = password; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getDelFlag() + { + return delFlag; + } + + public void setDelFlag(String delFlag) + { + this.delFlag = delFlag; + } + + public String getLoginIp() + { + return loginIp; + } + + public void setLoginIp(String loginIp) + { + this.loginIp = loginIp; + } + + public Date getLoginDate() + { + return loginDate; + } + + public void setLoginDate(Date loginDate) + { + this.loginDate = loginDate; + } + + public Date getPwdUpdateDate() + { + return pwdUpdateDate; + } + + public void setPwdUpdateDate(Date pwdUpdateDate) + { + this.pwdUpdateDate = pwdUpdateDate; + } + + public SysDept getDept() + { + return dept; + } + + public void setDept(SysDept dept) + { + this.dept = dept; + } + + public List getRoles() + { + return roles; + } + + public void setRoles(List roles) + { + this.roles = roles; + } + + public Long[] getRoleIds() + { + return roleIds; + } + + public void setRoleIds(Long[] roleIds) + { + this.roleIds = roleIds; + } + + public Long[] getPostIds() + { + return postIds; + } + + public void setPostIds(Long[] postIds) + { + this.postIds = postIds; + } + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("userId", getUserId()) + .append("deptId", getDeptId()) + .append("userName", getUserName()) + .append("nickName", getNickName()) + .append("email", getEmail()) + .append("phonenumber", getPhonenumber()) + .append("sex", getSex()) + .append("avatar", getAvatar()) + .append("password", getPassword()) + .append("status", getStatus()) + .append("delFlag", getDelFlag()) + .append("loginIp", getLoginIp()) + .append("loginDate", getLoginDate()) + .append("pwdUpdateDate", getPwdUpdateDate()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .append("dept", getDept()) + .toString(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java new file mode 100644 index 0000000..b5bc8c8 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java @@ -0,0 +1,69 @@ +package com.ruoyi.common.core.domain.model; + +/** + * 用户登录对象 + * + * @author ruoyi + */ +public class LoginBody +{ + /** + * 用户名 + */ + private String username; + + /** + * 用户密码 + */ + private String password; + + /** + * 验证码 + */ + private String code; + + /** + * 唯一标识 + */ + private String uuid; + + public String getUsername() + { + return username; + } + + public void setUsername(String username) + { + this.username = username; + } + + public String getPassword() + { + return password; + } + + public void setPassword(String password) + { + this.password = password; + } + + public String getCode() + { + return code; + } + + public void setCode(String code) + { + this.code = code; + } + + public String getUuid() + { + return uuid; + } + + public void setUuid(String uuid) + { + this.uuid = uuid; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java new file mode 100644 index 0000000..670e6b3 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginUser.java @@ -0,0 +1,266 @@ +package com.ruoyi.common.core.domain.model; + +import com.alibaba.fastjson2.annotation.JSONField; +import com.ruoyi.common.core.domain.entity.SysUser; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import java.util.Collection; +import java.util.Set; + +/** + * 登录用户身份权限 + * + * @author ruoyi + */ +public class LoginUser implements UserDetails +{ + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + private Long userId; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 用户唯一标识 + */ + private String token; + + /** + * 登录时间 + */ + private Long loginTime; + + /** + * 过期时间 + */ + private Long expireTime; + + /** + * 登录IP地址 + */ + private String ipaddr; + + /** + * 登录地点 + */ + private String loginLocation; + + /** + * 浏览器类型 + */ + private String browser; + + /** + * 操作系统 + */ + private String os; + + /** + * 权限列表 + */ + private Set permissions; + + /** + * 用户信息 + */ + private SysUser user; + + public LoginUser() + { + } + + public LoginUser(SysUser user, Set permissions) + { + this.user = user; + this.permissions = permissions; + } + + public LoginUser(Long userId, Long deptId, SysUser user, Set permissions) + { + this.userId = userId; + this.deptId = deptId; + this.user = user; + this.permissions = permissions; + } + + public Long getUserId() + { + return userId; + } + + public void setUserId(Long userId) + { + this.userId = userId; + } + + public Long getDeptId() + { + return deptId; + } + + public void setDeptId(Long deptId) + { + this.deptId = deptId; + } + + public String getToken() + { + return token; + } + + public void setToken(String token) + { + this.token = token; + } + + @JSONField(serialize = false) + @Override + public String getPassword() + { + return user.getPassword(); + } + + @Override + public String getUsername() + { + return user.getUserName(); + } + + /** + * 账户是否未过期,过期无法验证 + */ + @JSONField(serialize = false) + @Override + public boolean isAccountNonExpired() + { + return true; + } + + /** + * 指定用户是否解锁,锁定的用户无法进行身份验证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isAccountNonLocked() + { + return true; + } + + /** + * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isCredentialsNonExpired() + { + return true; + } + + /** + * 是否可用 ,禁用的用户不能身份验证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isEnabled() + { + return true; + } + + public Long getLoginTime() + { + return loginTime; + } + + public void setLoginTime(Long loginTime) + { + this.loginTime = loginTime; + } + + public String getIpaddr() + { + return ipaddr; + } + + public void setIpaddr(String ipaddr) + { + this.ipaddr = ipaddr; + } + + public String getLoginLocation() + { + return loginLocation; + } + + public void setLoginLocation(String loginLocation) + { + this.loginLocation = loginLocation; + } + + public String getBrowser() + { + return browser; + } + + public void setBrowser(String browser) + { + this.browser = browser; + } + + public String getOs() + { + return os; + } + + public void setOs(String os) + { + this.os = os; + } + + public Long getExpireTime() + { + return expireTime; + } + + public void setExpireTime(Long expireTime) + { + this.expireTime = expireTime; + } + + public Set getPermissions() + { + return permissions; + } + + public void setPermissions(Set permissions) + { + this.permissions = permissions; + } + + public SysUser getUser() + { + return user; + } + + public void setUser(SysUser user) + { + this.user = user; + } + + @Override + public Collection getAuthorities() + { + return null; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/RegisterBody.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/RegisterBody.java new file mode 100644 index 0000000..868a1fc --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/RegisterBody.java @@ -0,0 +1,11 @@ +package com.ruoyi.common.core.domain.model; + +/** + * 用户注册对象 + * + * @author ruoyi + */ +public class RegisterBody extends LoginBody +{ + +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/page/PageDomain.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/page/PageDomain.java new file mode 100644 index 0000000..8966cb4 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/page/PageDomain.java @@ -0,0 +1,101 @@ +package com.ruoyi.common.core.page; + +import com.ruoyi.common.utils.StringUtils; + +/** + * 分页数据 + * + * @author ruoyi + */ +public class PageDomain +{ + /** 当前记录起始索引 */ + private Integer pageNum; + + /** 每页显示记录数 */ + private Integer pageSize; + + /** 排序列 */ + private String orderByColumn; + + /** 排序的方向desc或者asc */ + private String isAsc = "asc"; + + /** 分页参数合理化 */ + private Boolean reasonable = true; + + public String getOrderBy() + { + if (StringUtils.isEmpty(orderByColumn)) + { + return ""; + } + return StringUtils.toUnderScoreCase(orderByColumn) + " " + isAsc; + } + + public Integer getPageNum() + { + return pageNum; + } + + public void setPageNum(Integer pageNum) + { + this.pageNum = pageNum; + } + + public Integer getPageSize() + { + return pageSize; + } + + public void setPageSize(Integer pageSize) + { + this.pageSize = pageSize; + } + + public String getOrderByColumn() + { + return orderByColumn; + } + + public void setOrderByColumn(String orderByColumn) + { + this.orderByColumn = orderByColumn; + } + + public String getIsAsc() + { + return isAsc; + } + + public void setIsAsc(String isAsc) + { + if (StringUtils.isNotEmpty(isAsc)) + { + // 兼容前端排序类型 + if ("ascending".equals(isAsc)) + { + isAsc = "asc"; + } + else if ("descending".equals(isAsc)) + { + isAsc = "desc"; + } + this.isAsc = isAsc; + } + } + + public Boolean getReasonable() + { + if (StringUtils.isNull(reasonable)) + { + return Boolean.TRUE; + } + return reasonable; + } + + public void setReasonable(Boolean reasonable) + { + this.reasonable = reasonable; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableDataInfo.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableDataInfo.java new file mode 100644 index 0000000..2fff93e --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableDataInfo.java @@ -0,0 +1,85 @@ +package com.ruoyi.common.core.page; + +import java.io.Serializable; +import java.util.List; + +/** + * 表格分页数据对象 + * + * @author ruoyi + */ +public class TableDataInfo implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 总记录数 */ + private long total; + + /** 列表数据 */ + private List rows; + + /** 消息状态码 */ + private int code; + + /** 消息内容 */ + private String msg; + + /** + * 表格数据对象 + */ + public TableDataInfo() + { + } + + /** + * 分页 + * + * @param list 列表数据 + * @param total 总记录数 + */ + public TableDataInfo(List list, long total) + { + this.rows = list; + this.total = total; + } + + public long getTotal() + { + return total; + } + + public void setTotal(long total) + { + this.total = total; + } + + public List getRows() + { + return rows; + } + + public void setRows(List rows) + { + this.rows = rows; + } + + public int getCode() + { + return code; + } + + public void setCode(int code) + { + this.code = code; + } + + public String getMsg() + { + return msg; + } + + public void setMsg(String msg) + { + this.msg = msg; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableSupport.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableSupport.java new file mode 100644 index 0000000..a120c30 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/page/TableSupport.java @@ -0,0 +1,56 @@ +package com.ruoyi.common.core.page; + +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.utils.ServletUtils; + +/** + * 表格数据处理 + * + * @author ruoyi + */ +public class TableSupport +{ + /** + * 当前记录起始索引 + */ + public static final String PAGE_NUM = "pageNum"; + + /** + * 每页显示记录数 + */ + public static final String PAGE_SIZE = "pageSize"; + + /** + * 排序列 + */ + public static final String ORDER_BY_COLUMN = "orderByColumn"; + + /** + * 排序的方向 "desc" 或者 "asc". + */ + public static final String IS_ASC = "isAsc"; + + /** + * 分页参数合理化 + */ + public static final String REASONABLE = "reasonable"; + + /** + * 封装分页对象 + */ + public static PageDomain getPageDomain() + { + PageDomain pageDomain = new PageDomain(); + pageDomain.setPageNum(Convert.toInt(ServletUtils.getParameter(PAGE_NUM), 1)); + pageDomain.setPageSize(Convert.toInt(ServletUtils.getParameter(PAGE_SIZE), 10)); + pageDomain.setOrderByColumn(ServletUtils.getParameter(ORDER_BY_COLUMN)); + pageDomain.setIsAsc(ServletUtils.getParameter(IS_ASC)); + pageDomain.setReasonable(ServletUtils.getParameterToBool(REASONABLE)); + return pageDomain; + } + + public static PageDomain buildPageRequest() + { + return getPageDomain(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java new file mode 100644 index 0000000..44e80d8 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java @@ -0,0 +1,268 @@ +package com.ruoyi.common.core.redis; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.BoundSetOperations; +import org.springframework.data.redis.core.HashOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Component; + +/** + * spring redis 工具类 + * + * @author ruoyi + **/ +@SuppressWarnings(value = { "unchecked", "rawtypes" }) +@Component +public class RedisCache +{ + @Autowired + public RedisTemplate redisTemplate; + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + */ + public void setCacheObject(final String key, final T value) + { + redisTemplate.opsForValue().set(key, value); + } + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + * @param timeout 时间 + * @param timeUnit 时间颗粒度 + */ + public void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) + { + redisTemplate.opsForValue().set(key, value, timeout, timeUnit); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @return true=设置成功;false=设置失败 + */ + public boolean expire(final String key, final long timeout) + { + return expire(key, timeout, TimeUnit.SECONDS); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @param unit 时间单位 + * @return true=设置成功;false=设置失败 + */ + public boolean expire(final String key, final long timeout, final TimeUnit unit) + { + return redisTemplate.expire(key, timeout, unit); + } + + /** + * 获取有效时间 + * + * @param key Redis键 + * @return 有效时间 + */ + public long getExpire(final String key) + { + return redisTemplate.getExpire(key); + } + + /** + * 判断 key是否存在 + * + * @param key 键 + * @return true 存在 false不存在 + */ + public Boolean hasKey(String key) + { + return redisTemplate.hasKey(key); + } + + /** + * 获得缓存的基本对象。 + * + * @param key 缓存键值 + * @return 缓存键值对应的数据 + */ + public T getCacheObject(final String key) + { + ValueOperations operation = redisTemplate.opsForValue(); + return operation.get(key); + } + + /** + * 删除单个对象 + * + * @param key + */ + public boolean deleteObject(final String key) + { + return redisTemplate.delete(key); + } + + /** + * 删除集合对象 + * + * @param collection 多个对象 + * @return + */ + public boolean deleteObject(final Collection collection) + { + return redisTemplate.delete(collection) > 0; + } + + /** + * 缓存List数据 + * + * @param key 缓存的键值 + * @param dataList 待缓存的List数据 + * @return 缓存的对象 + */ + public long setCacheList(final String key, final List dataList) + { + Long count = redisTemplate.opsForList().rightPushAll(key, dataList); + return count == null ? 0 : count; + } + + /** + * 获得缓存的list对象 + * + * @param key 缓存的键值 + * @return 缓存键值对应的数据 + */ + public List getCacheList(final String key) + { + return redisTemplate.opsForList().range(key, 0, -1); + } + + /** + * 缓存Set + * + * @param key 缓存键值 + * @param dataSet 缓存的数据 + * @return 缓存数据的对象 + */ + public BoundSetOperations setCacheSet(final String key, final Set dataSet) + { + BoundSetOperations setOperation = redisTemplate.boundSetOps(key); + Iterator it = dataSet.iterator(); + while (it.hasNext()) + { + setOperation.add(it.next()); + } + return setOperation; + } + + /** + * 获得缓存的set + * + * @param key + * @return + */ + public Set getCacheSet(final String key) + { + return redisTemplate.opsForSet().members(key); + } + + /** + * 缓存Map + * + * @param key + * @param dataMap + */ + public void setCacheMap(final String key, final Map dataMap) + { + if (dataMap != null) { + redisTemplate.opsForHash().putAll(key, dataMap); + } + } + + /** + * 获得缓存的Map + * + * @param key + * @return + */ + public Map getCacheMap(final String key) + { + return redisTemplate.opsForHash().entries(key); + } + + /** + * 往Hash中存入数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @param value 值 + */ + public void setCacheMapValue(final String key, final String hKey, final T value) + { + redisTemplate.opsForHash().put(key, hKey, value); + } + + /** + * 获取Hash中的数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @return Hash中的对象 + */ + public T getCacheMapValue(final String key, final String hKey) + { + HashOperations opsForHash = redisTemplate.opsForHash(); + return opsForHash.get(key, hKey); + } + + /** + * 获取多个Hash中的数据 + * + * @param key Redis键 + * @param hKeys Hash键集合 + * @return Hash对象集合 + */ + public List getMultiCacheMapValue(final String key, final Collection hKeys) + { + return redisTemplate.opsForHash().multiGet(key, hKeys); + } + + /** + * 删除Hash中的某条数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @return 是否成功 + */ + public boolean deleteCacheMapValue(final String key, final String hKey) + { + return redisTemplate.opsForHash().delete(key, hKey) > 0; + } + + /** + * 获得缓存的基本对象列表 + * + * @param pattern 字符串前缀 + * @return 对象列表 + */ + public Collection keys(final String pattern) + { + return redisTemplate.keys(pattern); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/text/CharsetKit.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/text/CharsetKit.java new file mode 100644 index 0000000..84124aa --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/text/CharsetKit.java @@ -0,0 +1,86 @@ +package com.ruoyi.common.core.text; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import com.ruoyi.common.utils.StringUtils; + +/** + * 字符集工具类 + * + * @author ruoyi + */ +public class CharsetKit +{ + /** ISO-8859-1 */ + public static final String ISO_8859_1 = "ISO-8859-1"; + /** UTF-8 */ + public static final String UTF_8 = "UTF-8"; + /** GBK */ + public static final String GBK = "GBK"; + + /** ISO-8859-1 */ + public static final Charset CHARSET_ISO_8859_1 = Charset.forName(ISO_8859_1); + /** UTF-8 */ + public static final Charset CHARSET_UTF_8 = Charset.forName(UTF_8); + /** GBK */ + public static final Charset CHARSET_GBK = Charset.forName(GBK); + + /** + * 转换为Charset对象 + * + * @param charset 字符集,为空则返回默认字符集 + * @return Charset + */ + public static Charset charset(String charset) + { + return StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset); + } + + /** + * 转换字符串的字符集编码 + * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, String srcCharset, String destCharset) + { + return convert(source, Charset.forName(srcCharset), Charset.forName(destCharset)); + } + + /** + * 转换字符串的字符集编码 + * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, Charset srcCharset, Charset destCharset) + { + if (null == srcCharset) + { + srcCharset = StandardCharsets.ISO_8859_1; + } + + if (null == destCharset) + { + destCharset = StandardCharsets.UTF_8; + } + + if (StringUtils.isEmpty(source) || srcCharset.equals(destCharset)) + { + return source; + } + return new String(source.getBytes(srcCharset), destCharset); + } + + /** + * @return 系统字符集编码 + */ + public static String systemCharset() + { + return Charset.defaultCharset().name(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/text/Convert.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/text/Convert.java new file mode 100644 index 0000000..a87828a --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/text/Convert.java @@ -0,0 +1,1018 @@ +package com.ruoyi.common.core.text; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.text.NumberFormat; +import java.util.Set; +import com.ruoyi.common.utils.StringUtils; + +/** + * 类型转换器 + * + * @author ruoyi + */ +public class Convert +{ + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static String toStr(Object value, String defaultValue) + { + if (null == value) + { + return defaultValue; + } + if (value instanceof String) + { + return (String) value; + } + return value.toString(); + } + + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static String toStr(Object value) + { + return toStr(value, null); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Character toChar(Object value, Character defaultValue) + { + if (null == value) + { + return defaultValue; + } + if (value instanceof Character) + { + return (Character) value; + } + + final String valueStr = toStr(value, null); + return StringUtils.isEmpty(valueStr) ? defaultValue : valueStr.charAt(0); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Character toChar(Object value) + { + return toChar(value, null); + } + + /** + * 转换为byte
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Byte toByte(Object value, Byte defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Byte) + { + return (Byte) value; + } + if (value instanceof Number) + { + return ((Number) value).byteValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Byte.parseByte(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为byte
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Byte toByte(Object value) + { + return toByte(value, null); + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Short toShort(Object value, Short defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Short) + { + return (Short) value; + } + if (value instanceof Number) + { + return ((Number) value).shortValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Short.parseShort(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Short toShort(Object value) + { + return toShort(value, null); + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Number toNumber(Object value, Number defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Number) + { + return (Number) value; + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return NumberFormat.getInstance().parse(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Number toNumber(Object value) + { + return toNumber(value, null); + } + + /** + * 转换为int
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Integer toInt(Object value, Integer defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Integer) + { + return (Integer) value; + } + if (value instanceof Number) + { + return ((Number) value).intValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Integer.parseInt(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为int
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Integer toInt(Object value) + { + return toInt(value, null); + } + + /** + * 转换为Integer数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String str) + { + return toIntArray(",", str); + } + + /** + * 转换为Long数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String str) + { + return toLongArray(",", str); + } + + /** + * 转换为Integer数组
+ * + * @param split 分隔符 + * @param split 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String split, String str) + { + if (StringUtils.isEmpty(str)) + { + return new Integer[] {}; + } + String[] arr = str.split(split); + final Integer[] ints = new Integer[arr.length]; + for (int i = 0; i < arr.length; i++) + { + final Integer v = toInt(arr[i], 0); + ints[i] = v; + } + return ints; + } + + /** + * 转换为Long数组
+ * + * @param split 分隔符 + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String split, String str) + { + if (StringUtils.isEmpty(str)) + { + return new Long[] {}; + } + String[] arr = str.split(split); + final Long[] longs = new Long[arr.length]; + for (int i = 0; i < arr.length; i++) + { + final Long v = toLong(arr[i], null); + longs[i] = v; + } + return longs; + } + + /** + * 转换为String数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String str) + { + if (StringUtils.isEmpty(str)) + { + return new String[] {}; + } + return toStrArray(",", str); + } + + /** + * 转换为String数组
+ * + * @param split 分隔符 + * @param split 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String split, String str) + { + return str.split(split); + } + + /** + * 转换为long
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Long toLong(Object value, Long defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Long) + { + return (Long) value; + } + if (value instanceof Number) + { + return ((Number) value).longValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + // 支持科学计数法 + return new BigDecimal(valueStr.trim()).longValue(); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为long
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Long toLong(Object value) + { + return toLong(value, null); + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Double toDouble(Object value, Double defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Double) + { + return (Double) value; + } + if (value instanceof Number) + { + return ((Number) value).doubleValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + // 支持科学计数法 + return new BigDecimal(valueStr.trim()).doubleValue(); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Double toDouble(Object value) + { + return toDouble(value, null); + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Float toFloat(Object value, Float defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Float) + { + return (Float) value; + } + if (value instanceof Number) + { + return ((Number) value).floatValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Float.parseFloat(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Float toFloat(Object value) + { + return toFloat(value, null); + } + + /** + * 转换为boolean
+ * String支持的值为:true、false、yes、ok、no、1、0、是、否, 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Boolean toBool(Object value, Boolean defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Boolean) + { + return (Boolean) value; + } + String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + valueStr = valueStr.trim().toLowerCase(); + switch (valueStr) + { + case "true": + case "yes": + case "ok": + case "1": + case "是": + return true; + case "false": + case "no": + case "0": + case "否": + return false; + default: + return defaultValue; + } + } + + /** + * 转换为boolean
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Boolean toBool(Object value) + { + return toBool(value, null); + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * + * @param clazz Enum的Class + * @param value 值 + * @param defaultValue 默认值 + * @return Enum + */ + public static > E toEnum(Class clazz, Object value, E defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (clazz.isAssignableFrom(value.getClass())) + { + @SuppressWarnings("unchecked") + E myE = (E) value; + return myE; + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Enum.valueOf(clazz, valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * + * @param clazz Enum的Class + * @param value 值 + * @return Enum + */ + public static > E toEnum(Class clazz, Object value) + { + return toEnum(clazz, value, null); + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value, BigInteger defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof BigInteger) + { + return (BigInteger) value; + } + if (value instanceof Long) + { + return BigInteger.valueOf((Long) value); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return new BigInteger(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value) + { + return toBigInteger(value, null); + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value, BigDecimal defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof BigDecimal) + { + return (BigDecimal) value; + } + if (value instanceof Long) + { + return new BigDecimal((Long) value); + } + if (value instanceof Double) + { + return BigDecimal.valueOf((Double) value); + } + if (value instanceof Integer) + { + return new BigDecimal((Integer) value); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return new BigDecimal(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value) + { + return toBigDecimal(value, null); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @return 字符串 + */ + public static String utf8Str(Object obj) + { + return str(obj, CharsetKit.CHARSET_UTF_8); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @param charsetName 字符集 + * @return 字符串 + */ + public static String str(Object obj, String charsetName) + { + return str(obj, Charset.forName(charsetName)); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(Object obj, Charset charset) + { + if (null == obj) + { + return null; + } + + if (obj instanceof String) + { + return (String) obj; + } + else if (obj instanceof byte[] || obj instanceof Byte[]) + { + if (obj instanceof byte[]) + { + return str((byte[]) obj, charset); + } + else + { + Byte[] bytes = (Byte[]) obj; + int length = bytes.length; + byte[] dest = new byte[length]; + for (int i = 0; i < length; i++) + { + dest[i] = bytes[i]; + } + return str(dest, charset); + } + } + else if (obj instanceof ByteBuffer) + { + return str((ByteBuffer) obj, charset); + } + return obj.toString(); + } + + /** + * 将byte数组转为字符串 + * + * @param bytes byte数组 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(byte[] bytes, String charset) + { + return str(bytes, StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset)); + } + + /** + * 解码字节码 + * + * @param data 字符串 + * @param charset 字符集,如果此字段为空,则解码的结果取决于平台 + * @return 解码后的字符串 + */ + public static String str(byte[] data, Charset charset) + { + if (data == null) + { + return null; + } + + if (null == charset) + { + return new String(data); + } + return new String(data, charset); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, String charset) + { + if (data == null) + { + return null; + } + + return str(data, Charset.forName(charset)); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, Charset charset) + { + if (null == charset) + { + charset = Charset.defaultCharset(); + } + return charset.decode(data).toString(); + } + + // ----------------------------------------------------------------------- 全角半角转换 + /** + * 半角转全角 + * + * @param input String. + * @return 全角字符串. + */ + public static String toSBC(String input) + { + return toSBC(input, null); + } + + /** + * 半角转全角 + * + * @param input String + * @param notConvertSet 不替换的字符集合 + * @return 全角字符串. + */ + public static String toSBC(String input, Set notConvertSet) + { + char[] c = input.toCharArray(); + for (int i = 0; i < c.length; i++) + { + if (null != notConvertSet && notConvertSet.contains(c[i])) + { + // 跳过不替换的字符 + continue; + } + + if (c[i] == ' ') + { + c[i] = '\u3000'; + } + else if (c[i] < '\177') + { + c[i] = (char) (c[i] + 65248); + + } + } + return new String(c); + } + + /** + * 全角转半角 + * + * @param input String. + * @return 半角字符串 + */ + public static String toDBC(String input) + { + return toDBC(input, null); + } + + /** + * 替换全角为半角 + * + * @param text 文本 + * @param notConvertSet 不替换的字符集合 + * @return 替换后的字符 + */ + public static String toDBC(String text, Set notConvertSet) + { + char[] c = text.toCharArray(); + for (int i = 0; i < c.length; i++) + { + if (null != notConvertSet && notConvertSet.contains(c[i])) + { + // 跳过不替换的字符 + continue; + } + + if (c[i] == '\u3000') + { + c[i] = ' '; + } + else if (c[i] > '\uFF00' && c[i] < '\uFF5F') + { + c[i] = (char) (c[i] - 65248); + } + } + return new String(c); + } + + /** + * 数字金额大写转换 先写个完整的然后将如零拾替换成零 + * + * @param n 数字 + * @return 中文大写数字 + */ + public static String digitUppercase(double n) + { + String[] fraction = { "角", "分" }; + String[] digit = { "零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖" }; + String[][] unit = { { "元", "万", "亿" }, { "", "拾", "佰", "仟" } }; + + String head = n < 0 ? "负" : ""; + n = Math.abs(n); + + String s = ""; + for (int i = 0; i < fraction.length; i++) + { + // 优化double计算精度丢失问题 + BigDecimal nNum = new BigDecimal(n); + BigDecimal decimal = new BigDecimal(10); + BigDecimal scale = nNum.multiply(decimal).setScale(2, RoundingMode.HALF_EVEN); + double d = scale.doubleValue(); + s += (digit[(int) (Math.floor(d * Math.pow(10, i)) % 10)] + fraction[i]).replaceAll("(零.)+", ""); + } + if (s.length() < 1) + { + s = "整"; + } + int integerPart = (int) Math.floor(n); + + for (int i = 0; i < unit[0].length && integerPart > 0; i++) + { + String p = ""; + for (int j = 0; j < unit[1].length && n > 0; j++) + { + p = digit[integerPart % 10] + unit[1][j] + p; + integerPart = integerPart / 10; + } + s = p.replaceAll("(零.)*零$", "").replaceAll("^$", "零") + unit[0][i] + s; + } + return head + s.replaceAll("(零.)*零元", "元").replaceFirst("(零.)+", "").replaceAll("(零.)+", "零").replaceAll("^整$", "零元整"); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/text/StrFormatter.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/text/StrFormatter.java new file mode 100644 index 0000000..c78ac77 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/text/StrFormatter.java @@ -0,0 +1,92 @@ +package com.ruoyi.common.core.text; + +import com.ruoyi.common.utils.StringUtils; + +/** + * 字符串格式化 + * + * @author ruoyi + */ +public class StrFormatter +{ + public static final String EMPTY_JSON = "{}"; + public static final char C_BACKSLASH = '\\'; + public static final char C_DELIM_START = '{'; + public static final char C_DELIM_END = '}'; + + /** + * 格式化字符串
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param strPattern 字符串模板 + * @param argArray 参数列表 + * @return 结果 + */ + public static String format(final String strPattern, final Object... argArray) + { + if (StringUtils.isEmpty(strPattern) || StringUtils.isEmpty(argArray)) + { + return strPattern; + } + final int strPatternLength = strPattern.length(); + + // 初始化定义好的长度以获得更好的性能 + StringBuilder sbuf = new StringBuilder(strPatternLength + 50); + + int handledPosition = 0; + int delimIndex;// 占位符所在位置 + for (int argIndex = 0; argIndex < argArray.length; argIndex++) + { + delimIndex = strPattern.indexOf(EMPTY_JSON, handledPosition); + if (delimIndex == -1) + { + if (handledPosition == 0) + { + return strPattern; + } + else + { // 字符串模板剩余部分不再包含占位符,加入剩余部分后返回结果 + sbuf.append(strPattern, handledPosition, strPatternLength); + return sbuf.toString(); + } + } + else + { + if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == C_BACKSLASH) + { + if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == C_BACKSLASH) + { + // 转义符之前还有一个转义符,占位符依旧有效 + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(Convert.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + 2; + } + else + { + // 占位符被转义 + argIndex--; + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(C_DELIM_START); + handledPosition = delimIndex + 1; + } + } + else + { + // 正常占位符 + sbuf.append(strPattern, handledPosition, delimIndex); + sbuf.append(Convert.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + 2; + } + } + } + // 加入最后一个占位符后所有的字符 + sbuf.append(strPattern, handledPosition, strPattern.length()); + + return sbuf.toString(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessStatus.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessStatus.java new file mode 100644 index 0000000..10b7306 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessStatus.java @@ -0,0 +1,20 @@ +package com.ruoyi.common.enums; + +/** + * 操作状态 + * + * @author ruoyi + * + */ +public enum BusinessStatus +{ + /** + * 成功 + */ + SUCCESS, + + /** + * 失败 + */ + FAIL, +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessType.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessType.java new file mode 100644 index 0000000..2e17c4a --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/BusinessType.java @@ -0,0 +1,59 @@ +package com.ruoyi.common.enums; + +/** + * 业务操作类型 + * + * @author ruoyi + */ +public enum BusinessType +{ + /** + * 其它 + */ + OTHER, + + /** + * 新增 + */ + INSERT, + + /** + * 修改 + */ + UPDATE, + + /** + * 删除 + */ + DELETE, + + /** + * 授权 + */ + GRANT, + + /** + * 导出 + */ + EXPORT, + + /** + * 导入 + */ + IMPORT, + + /** + * 强退 + */ + FORCE, + + /** + * 生成代码 + */ + GENCODE, + + /** + * 清空数据 + */ + CLEAN, +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/DataSourceType.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/DataSourceType.java new file mode 100644 index 0000000..0d945be --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/DataSourceType.java @@ -0,0 +1,19 @@ +package com.ruoyi.common.enums; + +/** + * 数据源 + * + * @author ruoyi + */ +public enum DataSourceType +{ + /** + * 主库 + */ + MASTER, + + /** + * 从库 + */ + SLAVE +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/DesensitizedType.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/DesensitizedType.java new file mode 100644 index 0000000..07f02ee --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/DesensitizedType.java @@ -0,0 +1,59 @@ +package com.ruoyi.common.enums; + +import java.util.function.Function; +import com.ruoyi.common.utils.DesensitizedUtil; + +/** + * 脱敏类型 + * + * @author ruoyi + */ +public enum DesensitizedType +{ + /** + * 姓名,第2位星号替换 + */ + USERNAME(s -> s.replaceAll("(\\S)\\S(\\S*)", "$1*$2")), + + /** + * 密码,全部字符都用*代替 + */ + PASSWORD(DesensitizedUtil::password), + + /** + * 身份证,中间10位星号替换 + */ + ID_CARD(s -> s.replaceAll("(\\d{4})\\d{10}(\\d{3}[Xx]|\\d{4})", "$1** **** ****$2")), + + /** + * 手机号,中间4位星号替换 + */ + PHONE(s -> s.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")), + + /** + * 电子邮箱,仅显示第一个字母和@后面的地址显示,其他星号替换 + */ + EMAIL(s -> s.replaceAll("(^.)[^@]*(@.*$)", "$1****$2")), + + /** + * 银行卡号,保留最后4位,其他星号替换 + */ + BANK_CARD(s -> s.replaceAll("\\d{15}(\\d{3})", "**** **** **** **** $1")), + + /** + * 车牌号码,包含普通车辆、新能源车辆 + */ + CAR_LICENSE(DesensitizedUtil::carLicense); + + private final Function desensitizer; + + DesensitizedType(Function desensitizer) + { + this.desensitizer = desensitizer; + } + + public Function desensitizer() + { + return desensitizer; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/HttpMethod.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/HttpMethod.java new file mode 100644 index 0000000..be6f739 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/HttpMethod.java @@ -0,0 +1,36 @@ +package com.ruoyi.common.enums; + +import java.util.HashMap; +import java.util.Map; +import org.springframework.lang.Nullable; + +/** + * 请求方式 + * + * @author ruoyi + */ +public enum HttpMethod +{ + GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE; + + private static final Map mappings = new HashMap<>(16); + + static + { + for (HttpMethod httpMethod : values()) + { + mappings.put(httpMethod.name(), httpMethod); + } + } + + @Nullable + public static HttpMethod resolve(@Nullable String method) + { + return (method != null ? mappings.get(method) : null); + } + + public boolean matches(String method) + { + return (this == resolve(method)); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/LimitType.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/LimitType.java new file mode 100644 index 0000000..c609fd8 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/LimitType.java @@ -0,0 +1,20 @@ +package com.ruoyi.common.enums; + +/** + * 限流类型 + * + * @author ruoyi + */ + +public enum LimitType +{ + /** + * 默认策略全局限流 + */ + DEFAULT, + + /** + * 根据请求者IP进行限流 + */ + IP +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/OperatorType.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/OperatorType.java new file mode 100644 index 0000000..bdd143c --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/OperatorType.java @@ -0,0 +1,24 @@ +package com.ruoyi.common.enums; + +/** + * 操作人类别 + * + * @author ruoyi + */ +public enum OperatorType +{ + /** + * 其它 + */ + OTHER, + + /** + * 后台用户 + */ + MANAGE, + + /** + * 手机端用户 + */ + MOBILE +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/UserStatus.java b/ruoyi-common/src/main/java/com/ruoyi/common/enums/UserStatus.java new file mode 100644 index 0000000..d7ff44a --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/enums/UserStatus.java @@ -0,0 +1,30 @@ +package com.ruoyi.common.enums; + +/** + * 用户状态 + * + * @author ruoyi + */ +public enum UserStatus +{ + OK("0", "正常"), DISABLE("1", "停用"), DELETED("2", "删除"); + + private final String code; + private final String info; + + UserStatus(String code, String info) + { + this.code = code; + this.info = info; + } + + public String getCode() + { + return code; + } + + public String getInfo() + { + return info; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/DemoModeException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/DemoModeException.java new file mode 100644 index 0000000..f6ad2ab --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/DemoModeException.java @@ -0,0 +1,15 @@ +package com.ruoyi.common.exception; + +/** + * 演示模式异常 + * + * @author ruoyi + */ +public class DemoModeException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public DemoModeException() + { + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/GlobalException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/GlobalException.java new file mode 100644 index 0000000..81a71b5 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/GlobalException.java @@ -0,0 +1,58 @@ +package com.ruoyi.common.exception; + +/** + * 全局异常 + * + * @author ruoyi + */ +public class GlobalException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + /** + * 错误提示 + */ + private String message; + + /** + * 错误明细,内部调试错误 + * + * 和 {@link CommonResult#getDetailMessage()} 一致的设计 + */ + private String detailMessage; + + /** + * 空构造方法,避免反序列化问题 + */ + public GlobalException() + { + } + + public GlobalException(String message) + { + this.message = message; + } + + public String getDetailMessage() + { + return detailMessage; + } + + public GlobalException setDetailMessage(String detailMessage) + { + this.detailMessage = detailMessage; + return this; + } + + @Override + public String getMessage() + { + return message; + } + + public GlobalException setMessage(String message) + { + this.message = message; + return this; + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/ServiceException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/ServiceException.java new file mode 100644 index 0000000..fcc7ab6 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/ServiceException.java @@ -0,0 +1,74 @@ +package com.ruoyi.common.exception; + +/** + * 业务异常 + * + * @author ruoyi + */ +public final class ServiceException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + /** + * 错误码 + */ + private Integer code; + + /** + * 错误提示 + */ + private String message; + + /** + * 错误明细,内部调试错误 + * + * 和 {@link CommonResult#getDetailMessage()} 一致的设计 + */ + private String detailMessage; + + /** + * 空构造方法,避免反序列化问题 + */ + public ServiceException() + { + } + + public ServiceException(String message) + { + this.message = message; + } + + public ServiceException(String message, Integer code) + { + this.message = message; + this.code = code; + } + + public String getDetailMessage() + { + return detailMessage; + } + + @Override + public String getMessage() + { + return message; + } + + public Integer getCode() + { + return code; + } + + public ServiceException setMessage(String message) + { + this.message = message; + return this; + } + + public ServiceException setDetailMessage(String detailMessage) + { + this.detailMessage = detailMessage; + return this; + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/UtilException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/UtilException.java new file mode 100644 index 0000000..980fa46 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/UtilException.java @@ -0,0 +1,26 @@ +package com.ruoyi.common.exception; + +/** + * 工具类异常 + * + * @author ruoyi + */ +public class UtilException extends RuntimeException +{ + private static final long serialVersionUID = 8247610319171014183L; + + public UtilException(Throwable e) + { + super(e.getMessage(), e); + } + + public UtilException(String message) + { + super(message); + } + + public UtilException(String message, Throwable throwable) + { + super(message, throwable); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/base/BaseException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/base/BaseException.java new file mode 100644 index 0000000..b55d72e --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/base/BaseException.java @@ -0,0 +1,97 @@ +package com.ruoyi.common.exception.base; + +import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.StringUtils; + +/** + * 基础异常 + * + * @author ruoyi + */ +public class BaseException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + /** + * 所属模块 + */ + private String module; + + /** + * 错误码 + */ + private String code; + + /** + * 错误码对应的参数 + */ + private Object[] args; + + /** + * 错误消息 + */ + private String defaultMessage; + + public BaseException(String module, String code, Object[] args, String defaultMessage) + { + this.module = module; + this.code = code; + this.args = args; + this.defaultMessage = defaultMessage; + } + + public BaseException(String module, String code, Object[] args) + { + this(module, code, args, null); + } + + public BaseException(String module, String defaultMessage) + { + this(module, null, null, defaultMessage); + } + + public BaseException(String code, Object[] args) + { + this(null, code, args, null); + } + + public BaseException(String defaultMessage) + { + this(null, null, null, defaultMessage); + } + + @Override + public String getMessage() + { + String message = null; + if (!StringUtils.isEmpty(code)) + { + message = MessageUtils.message(code, args); + } + if (message == null) + { + message = defaultMessage; + } + return message; + } + + public String getModule() + { + return module; + } + + public String getCode() + { + return code; + } + + public Object[] getArgs() + { + return args; + } + + public String getDefaultMessage() + { + return defaultMessage; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileException.java new file mode 100644 index 0000000..871f09b --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileException.java @@ -0,0 +1,19 @@ +package com.ruoyi.common.exception.file; + +import com.ruoyi.common.exception.base.BaseException; + +/** + * 文件信息异常类 + * + * @author ruoyi + */ +public class FileException extends BaseException +{ + private static final long serialVersionUID = 1L; + + public FileException(String code, Object[] args) + { + super("file", code, args, null); + } + +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileNameLengthLimitExceededException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileNameLengthLimitExceededException.java new file mode 100644 index 0000000..70e0ec9 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileNameLengthLimitExceededException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.file; + +/** + * 文件名称超长限制异常类 + * + * @author ruoyi + */ +public class FileNameLengthLimitExceededException extends FileException +{ + private static final long serialVersionUID = 1L; + + public FileNameLengthLimitExceededException(int defaultFileNameLength) + { + super("upload.filename.exceed.length", new Object[] { defaultFileNameLength }); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileSizeLimitExceededException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileSizeLimitExceededException.java new file mode 100644 index 0000000..ec6ab05 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileSizeLimitExceededException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.file; + +/** + * 文件名大小限制异常类 + * + * @author ruoyi + */ +public class FileSizeLimitExceededException extends FileException +{ + private static final long serialVersionUID = 1L; + + public FileSizeLimitExceededException(long defaultMaxSize) + { + super("upload.exceed.maxSize", new Object[] { defaultMaxSize }); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileUploadException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileUploadException.java new file mode 100644 index 0000000..f45e7ef --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileUploadException.java @@ -0,0 +1,61 @@ +package com.ruoyi.common.exception.file; + +import java.io.PrintStream; +import java.io.PrintWriter; + +/** + * 文件上传异常类 + * + * @author ruoyi + */ +public class FileUploadException extends Exception +{ + + private static final long serialVersionUID = 1L; + + private final Throwable cause; + + public FileUploadException() + { + this(null, null); + } + + public FileUploadException(final String msg) + { + this(msg, null); + } + + public FileUploadException(String msg, Throwable cause) + { + super(msg); + this.cause = cause; + } + + @Override + public void printStackTrace(PrintStream stream) + { + super.printStackTrace(stream); + if (cause != null) + { + stream.println("Caused by:"); + cause.printStackTrace(stream); + } + } + + @Override + public void printStackTrace(PrintWriter writer) + { + super.printStackTrace(writer); + if (cause != null) + { + writer.println("Caused by:"); + cause.printStackTrace(writer); + } + } + + @Override + public Throwable getCause() + { + return cause; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/InvalidExtensionException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/InvalidExtensionException.java new file mode 100644 index 0000000..011f308 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/file/InvalidExtensionException.java @@ -0,0 +1,80 @@ +package com.ruoyi.common.exception.file; + +import java.util.Arrays; + +/** + * 文件上传 误异常类 + * + * @author ruoyi + */ +public class InvalidExtensionException extends FileUploadException +{ + private static final long serialVersionUID = 1L; + + private String[] allowedExtension; + private String extension; + private String filename; + + public InvalidExtensionException(String[] allowedExtension, String extension, String filename) + { + super("文件[" + filename + "]后缀[" + extension + "]不正确,请上传" + Arrays.toString(allowedExtension) + "格式"); + this.allowedExtension = allowedExtension; + this.extension = extension; + this.filename = filename; + } + + public String[] getAllowedExtension() + { + return allowedExtension; + } + + public String getExtension() + { + return extension; + } + + public String getFilename() + { + return filename; + } + + public static class InvalidImageExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidImageExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidFlashExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidFlashExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidMediaExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidMediaExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidVideoExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidVideoExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/job/TaskException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/job/TaskException.java new file mode 100644 index 0000000..a567b40 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/job/TaskException.java @@ -0,0 +1,34 @@ +package com.ruoyi.common.exception.job; + +/** + * 计划策略异常 + * + * @author ruoyi + */ +public class TaskException extends Exception +{ + private static final long serialVersionUID = 1L; + + private Code code; + + public TaskException(String msg, Code code) + { + this(msg, code, null); + } + + public TaskException(String msg, Code code, Exception nestedEx) + { + super(msg, nestedEx); + this.code = code; + } + + public Code getCode() + { + return code; + } + + public enum Code + { + TASK_EXISTS, NO_TASK_EXISTS, TASK_ALREADY_STARTED, UNKNOWN, CONFIG_ERROR, TASK_NODE_NOT_AVAILABLE + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/BlackListException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/BlackListException.java new file mode 100644 index 0000000..2bf5038 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/BlackListException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.user; + +/** + * 黑名单IP异常类 + * + * @author ruoyi + */ +public class BlackListException extends UserException +{ + private static final long serialVersionUID = 1L; + + public BlackListException() + { + super("login.blocked", null); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaException.java new file mode 100644 index 0000000..389dbc7 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.user; + +/** + * 验证码错误异常类 + * + * @author ruoyi + */ +public class CaptchaException extends UserException +{ + private static final long serialVersionUID = 1L; + + public CaptchaException() + { + super("user.jcaptcha.error", null); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaExpireException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaExpireException.java new file mode 100644 index 0000000..85f9486 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/CaptchaExpireException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.user; + +/** + * 验证码失效异常类 + * + * @author ruoyi + */ +public class CaptchaExpireException extends UserException +{ + private static final long serialVersionUID = 1L; + + public CaptchaExpireException() + { + super("user.jcaptcha.expire", null); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserException.java new file mode 100644 index 0000000..c292d70 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserException.java @@ -0,0 +1,18 @@ +package com.ruoyi.common.exception.user; + +import com.ruoyi.common.exception.base.BaseException; + +/** + * 用户信息异常类 + * + * @author ruoyi + */ +public class UserException extends BaseException +{ + private static final long serialVersionUID = 1L; + + public UserException(String code, Object[] args) + { + super("user", code, args, null); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserNotExistsException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserNotExistsException.java new file mode 100644 index 0000000..eff8181 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserNotExistsException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.user; + +/** + * 用户不存在异常类 + * + * @author ruoyi + */ +public class UserNotExistsException extends UserException +{ + private static final long serialVersionUID = 1L; + + public UserNotExistsException() + { + super("user.not.exists", null); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordNotMatchException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordNotMatchException.java new file mode 100644 index 0000000..a7f3e5f --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordNotMatchException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.user; + +/** + * 用户密码不正确或不符合规范异常类 + * + * @author ruoyi + */ +public class UserPasswordNotMatchException extends UserException +{ + private static final long serialVersionUID = 1L; + + public UserPasswordNotMatchException() + { + super("user.password.not.match", null); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.java b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.java new file mode 100644 index 0000000..c887cf1 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.user; + +/** + * 用户错误最大次数异常类 + * + * @author ruoyi + */ +public class UserPasswordRetryLimitExceedException extends UserException +{ + private static final long serialVersionUID = 1L; + + public UserPasswordRetryLimitExceedException(int retryLimitCount, int lockTime) + { + super("user.password.retry.limit.exceed", new Object[] { retryLimitCount, lockTime }); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/PropertyPreExcludeFilter.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/PropertyPreExcludeFilter.java new file mode 100644 index 0000000..e1e431b --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/PropertyPreExcludeFilter.java @@ -0,0 +1,24 @@ +package com.ruoyi.common.filter; + +import com.alibaba.fastjson2.filter.SimplePropertyPreFilter; + +/** + * 排除JSON敏感属性 + * + * @author ruoyi + */ +public class PropertyPreExcludeFilter extends SimplePropertyPreFilter +{ + public PropertyPreExcludeFilter() + { + } + + public PropertyPreExcludeFilter addExcludes(String... filters) + { + for (int i = 0; i < filters.length; i++) + { + this.getExcludes().add(filters[i]); + } + return this; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatableFilter.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatableFilter.java new file mode 100644 index 0000000..a1bcfe2 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatableFilter.java @@ -0,0 +1,52 @@ +package com.ruoyi.common.filter; + +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import org.springframework.http.MediaType; +import com.ruoyi.common.utils.StringUtils; + +/** + * Repeatable 过滤器 + * + * @author ruoyi + */ +public class RepeatableFilter implements Filter +{ + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException + { + ServletRequest requestWrapper = null; + if (request instanceof HttpServletRequest + && StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) + { + requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response); + } + if (null == requestWrapper) + { + chain.doFilter(request, response); + } + else + { + chain.doFilter(requestWrapper, response); + } + } + + @Override + public void destroy() + { + + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatedlyRequestWrapper.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatedlyRequestWrapper.java new file mode 100644 index 0000000..407d1ba --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatedlyRequestWrapper.java @@ -0,0 +1,76 @@ +package com.ruoyi.common.filter; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import com.ruoyi.common.utils.http.HttpHelper; +import com.ruoyi.common.constant.Constants; + +/** + * 构建可重复读取inputStream的request + * + * @author ruoyi + */ +public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper +{ + private final byte[] body; + + public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException + { + super(request); + request.setCharacterEncoding(Constants.UTF8); + response.setCharacterEncoding(Constants.UTF8); + + body = HttpHelper.getBodyString(request).getBytes(Constants.UTF8); + } + + @Override + public BufferedReader getReader() throws IOException + { + return new BufferedReader(new InputStreamReader(getInputStream())); + } + + @Override + public ServletInputStream getInputStream() throws IOException + { + final ByteArrayInputStream bais = new ByteArrayInputStream(body); + return new ServletInputStream() + { + @Override + public int read() throws IOException + { + return bais.read(); + } + + @Override + public int available() throws IOException + { + return body.length; + } + + @Override + public boolean isFinished() + { + return false; + } + + @Override + public boolean isReady() + { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) + { + + } + }; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssFilter.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssFilter.java new file mode 100644 index 0000000..5c4cbe4 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssFilter.java @@ -0,0 +1,75 @@ +package com.ruoyi.common.filter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.enums.HttpMethod; + +/** + * 防止XSS攻击的过滤器 + * + * @author ruoyi + */ +public class XssFilter implements Filter +{ + /** + * 排除链接 + */ + public List excludes = new ArrayList<>(); + + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + String tempExcludes = filterConfig.getInitParameter("excludes"); + if (StringUtils.isNotEmpty(tempExcludes)) + { + String[] urls = tempExcludes.split(","); + for (String url : urls) + { + excludes.add(url); + } + } + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException + { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + if (handleExcludeURL(req, resp)) + { + chain.doFilter(request, response); + return; + } + XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request); + chain.doFilter(xssRequest, response); + } + + private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response) + { + String url = request.getServletPath(); + String method = request.getMethod(); + // GET DELETE 不过滤 + if (method == null || HttpMethod.GET.matches(method) || HttpMethod.DELETE.matches(method)) + { + return true; + } + return StringUtils.matches(url, excludes); + } + + @Override + public void destroy() + { + + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssHttpServletRequestWrapper.java b/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssHttpServletRequestWrapper.java new file mode 100644 index 0000000..05149f0 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/filter/XssHttpServletRequestWrapper.java @@ -0,0 +1,111 @@ +package com.ruoyi.common.filter; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import org.apache.commons.io.IOUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.html.EscapeUtil; + +/** + * XSS过滤处理 + * + * @author ruoyi + */ +public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper +{ + /** + * @param request + */ + public XssHttpServletRequestWrapper(HttpServletRequest request) + { + super(request); + } + + @Override + public String[] getParameterValues(String name) + { + String[] values = super.getParameterValues(name); + if (values != null) + { + int length = values.length; + String[] escapesValues = new String[length]; + for (int i = 0; i < length; i++) + { + // 防xss攻击和过滤前后空格 + escapesValues[i] = EscapeUtil.clean(values[i]).trim(); + } + return escapesValues; + } + return super.getParameterValues(name); + } + + @Override + public ServletInputStream getInputStream() throws IOException + { + // 非json类型,直接返回 + if (!isJsonRequest()) + { + return super.getInputStream(); + } + + // 为空,直接返回 + String json = IOUtils.toString(super.getInputStream(), "utf-8"); + if (StringUtils.isEmpty(json)) + { + return super.getInputStream(); + } + + // xss过滤 + json = EscapeUtil.clean(json).trim(); + byte[] jsonBytes = json.getBytes("utf-8"); + final ByteArrayInputStream bis = new ByteArrayInputStream(jsonBytes); + return new ServletInputStream() + { + @Override + public boolean isFinished() + { + return true; + } + + @Override + public boolean isReady() + { + return true; + } + + @Override + public int available() throws IOException + { + return jsonBytes.length; + } + + @Override + public void setReadListener(ReadListener readListener) + { + } + + @Override + public int read() throws IOException + { + return bis.read(); + } + }; + } + + /** + * 是否是Json请求 + * + * @param request + */ + public boolean isJsonRequest() + { + String header = super.getHeader(HttpHeaders.CONTENT_TYPE); + return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE); + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/Arith.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/Arith.java new file mode 100644 index 0000000..9f95c0f --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/Arith.java @@ -0,0 +1,113 @@ +package com.ruoyi.common.utils; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * 精确的浮点数运算 + * + * @author ruoyi + */ +public class Arith +{ + + /** 默认除法运算精度 */ + private static final int DEF_DIV_SCALE = 10; + + /** 这个类不能实例化 */ + private Arith() + { + } + + /** + * 提供精确的加法运算。 + * @param v1 被加数 + * @param v2 加数 + * @return 两个参数的和 + */ + public static double add(double v1, double v2) + { + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + return b1.add(b2).doubleValue(); + } + + /** + * 提供精确的减法运算。 + * @param v1 被减数 + * @param v2 减数 + * @return 两个参数的差 + */ + public static double sub(double v1, double v2) + { + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + return b1.subtract(b2).doubleValue(); + } + + /** + * 提供精确的乘法运算。 + * @param v1 被乘数 + * @param v2 乘数 + * @return 两个参数的积 + */ + public static double mul(double v1, double v2) + { + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + return b1.multiply(b2).doubleValue(); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到 + * 小数点以后10位,以后的数字四舍五入。 + * @param v1 被除数 + * @param v2 除数 + * @return 两个参数的商 + */ + public static double div(double v1, double v2) + { + return div(v1, v2, DEF_DIV_SCALE); + } + + /** + * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指 + * 定精度,以后的数字四舍五入。 + * @param v1 被除数 + * @param v2 除数 + * @param scale 表示表示需要精确到小数点以后几位。 + * @return 两个参数的商 + */ + public static double div(double v1, double v2, int scale) + { + if (scale < 0) + { + throw new IllegalArgumentException( + "The scale must be a positive integer or zero"); + } + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + if (b1.compareTo(BigDecimal.ZERO) == 0) + { + return BigDecimal.ZERO.doubleValue(); + } + return b1.divide(b2, scale, RoundingMode.HALF_UP).doubleValue(); + } + + /** + * 提供精确的小数位四舍五入处理。 + * @param v 需要四舍五入的数字 + * @param scale 小数点后保留几位 + * @return 四舍五入后的结果 + */ + public static double round(double v, int scale) + { + if (scale < 0) + { + throw new IllegalArgumentException( + "The scale must be a positive integer or zero"); + } + BigDecimal b = new BigDecimal(Double.toString(v)); + return b.divide(BigDecimal.ONE, scale, RoundingMode.HALF_UP).doubleValue(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/DateUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/DateUtils.java new file mode 100644 index 0000000..fb2ae21 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/DateUtils.java @@ -0,0 +1,191 @@ +package com.ruoyi.common.utils; + +import java.lang.management.ManagementFactory; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Date; +import org.apache.commons.lang3.time.DateFormatUtils; + +/** + * 时间工具类 + * + * @author ruoyi + */ +public class DateUtils extends org.apache.commons.lang3.time.DateUtils +{ + public static String YYYY = "yyyy"; + + public static String YYYY_MM = "yyyy-MM"; + + public static String YYYY_MM_DD = "yyyy-MM-dd"; + + public static String YYYYMMDDHHMMSS = "yyyyMMddHHmmss"; + + public static String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss"; + + private static String[] parsePatterns = { + "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM", + "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM", + "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"}; + + /** + * 获取当前Date型日期 + * + * @return Date() 当前日期 + */ + public static Date getNowDate() + { + return new Date(); + } + + /** + * 获取当前日期, 默认格式为yyyy-MM-dd + * + * @return String + */ + public static String getDate() + { + return dateTimeNow(YYYY_MM_DD); + } + + public static final String getTime() + { + return dateTimeNow(YYYY_MM_DD_HH_MM_SS); + } + + public static final String dateTimeNow() + { + return dateTimeNow(YYYYMMDDHHMMSS); + } + + public static final String dateTimeNow(final String format) + { + return parseDateToStr(format, new Date()); + } + + public static final String dateTime(final Date date) + { + return parseDateToStr(YYYY_MM_DD, date); + } + + public static final String parseDateToStr(final String format, final Date date) + { + return new SimpleDateFormat(format).format(date); + } + + public static final Date dateTime(final String format, final String ts) + { + try + { + return new SimpleDateFormat(format).parse(ts); + } + catch (ParseException e) + { + throw new RuntimeException(e); + } + } + + /** + * 日期路径 即年/月/日 如2018/08/08 + */ + public static final String datePath() + { + Date now = new Date(); + return DateFormatUtils.format(now, "yyyy/MM/dd"); + } + + /** + * 日期路径 即年/月/日 如20180808 + */ + public static final String dateTime() + { + Date now = new Date(); + return DateFormatUtils.format(now, "yyyyMMdd"); + } + + /** + * 日期型字符串转化为日期 格式 + */ + public static Date parseDate(Object str) + { + if (str == null) + { + return null; + } + try + { + return parseDate(str.toString(), parsePatterns); + } + catch (ParseException e) + { + return null; + } + } + + /** + * 获取服务器启动时间 + */ + public static Date getServerStartDate() + { + long time = ManagementFactory.getRuntimeMXBean().getStartTime(); + return new Date(time); + } + + /** + * 计算相差天数 + */ + public static int differentDaysByMillisecond(Date date1, Date date2) + { + return Math.abs((int) ((date2.getTime() - date1.getTime()) / (1000 * 3600 * 24))); + } + + /** + * 计算时间差 + * + * @param endDate 最后时间 + * @param startTime 开始时间 + * @return 时间差(天/小时/分钟) + */ + public static String timeDistance(Date endDate, Date startTime) + { + long nd = 1000 * 24 * 60 * 60; + long nh = 1000 * 60 * 60; + long nm = 1000 * 60; + // long ns = 1000; + // 获得两个时间的毫秒时间差异 + long diff = endDate.getTime() - startTime.getTime(); + // 计算差多少天 + long day = diff / nd; + // 计算差多少小时 + long hour = diff % nd / nh; + // 计算差多少分钟 + long min = diff % nd % nh / nm; + // 计算差多少秒//输出结果 + // long sec = diff % nd % nh % nm / ns; + return day + "天" + hour + "小时" + min + "分钟"; + } + + /** + * 增加 LocalDateTime ==> Date + */ + public static Date toDate(LocalDateTime temporalAccessor) + { + ZonedDateTime zdt = temporalAccessor.atZone(ZoneId.systemDefault()); + return Date.from(zdt.toInstant()); + } + + /** + * 增加 LocalDate ==> Date + */ + public static Date toDate(LocalDate temporalAccessor) + { + LocalDateTime localDateTime = LocalDateTime.of(temporalAccessor, LocalTime.of(0, 0, 0)); + ZonedDateTime zdt = localDateTime.atZone(ZoneId.systemDefault()); + return Date.from(zdt.toInstant()); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/DesensitizedUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/DesensitizedUtil.java new file mode 100644 index 0000000..f8a4c02 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/DesensitizedUtil.java @@ -0,0 +1,49 @@ +package com.ruoyi.common.utils; + +/** + * 脱敏工具类 + * + * @author ruoyi + */ +public class DesensitizedUtil +{ + /** + * 密码的全部字符都用*代替,比如:****** + * + * @param password 密码 + * @return 脱敏后的密码 + */ + public static String password(String password) + { + if (StringUtils.isBlank(password)) + { + return StringUtils.EMPTY; + } + return StringUtils.repeat('*', password.length()); + } + + /** + * 车牌中间用*代替,如果是错误的车牌,不处理 + * + * @param carLicense 完整的车牌号 + * @return 脱敏后的车牌 + */ + public static String carLicense(String carLicense) + { + if (StringUtils.isBlank(carLicense)) + { + return StringUtils.EMPTY; + } + // 普通车牌 + if (carLicense.length() == 7) + { + carLicense = StringUtils.hide(carLicense, 3, 6); + } + else if (carLicense.length() == 8) + { + // 新能源车牌 + carLicense = StringUtils.hide(carLicense, 3, 7); + } + return carLicense; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java new file mode 100644 index 0000000..f198462 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java @@ -0,0 +1,239 @@ +package com.ruoyi.common.utils; + +import java.util.Collection; +import java.util.List; +import com.alibaba.fastjson2.JSONArray; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.spring.SpringUtils; + +/** + * 字典工具类 + * + * @author ruoyi + */ +public class DictUtils +{ + /** + * 分隔符 + */ + public static final String SEPARATOR = ","; + + /** + * 设置字典缓存 + * + * @param key 参数键 + * @param dictDatas 字典数据列表 + */ + public static void setDictCache(String key, List dictDatas) + { + SpringUtils.getBean(RedisCache.class).setCacheObject(getCacheKey(key), dictDatas); + } + + /** + * 获取字典缓存 + * + * @param key 参数键 + * @return dictDatas 字典数据列表 + */ + public static List getDictCache(String key) + { + JSONArray arrayCache = SpringUtils.getBean(RedisCache.class).getCacheObject(getCacheKey(key)); + if (StringUtils.isNotNull(arrayCache)) + { + return arrayCache.toList(SysDictData.class); + } + return null; + } + + /** + * 根据字典类型和字典值获取字典标签 + * + * @param dictType 字典类型 + * @param dictValue 字典值 + * @return 字典标签 + */ + public static String getDictLabel(String dictType, String dictValue) + { + if (StringUtils.isEmpty(dictValue)) + { + return StringUtils.EMPTY; + } + return getDictLabel(dictType, dictValue, SEPARATOR); + } + + /** + * 根据字典类型和字典标签获取字典值 + * + * @param dictType 字典类型 + * @param dictLabel 字典标签 + * @return 字典值 + */ + public static String getDictValue(String dictType, String dictLabel) + { + if (StringUtils.isEmpty(dictLabel)) + { + return StringUtils.EMPTY; + } + return getDictValue(dictType, dictLabel, SEPARATOR); + } + + /** + * 根据字典类型和字典值获取字典标签 + * + * @param dictType 字典类型 + * @param dictValue 字典值 + * @param separator 分隔符 + * @return 字典标签 + */ + public static String getDictLabel(String dictType, String dictValue, String separator) + { + StringBuilder propertyString = new StringBuilder(); + List datas = getDictCache(dictType); + if (StringUtils.isNull(datas)) + { + return StringUtils.EMPTY; + } + if (StringUtils.containsAny(separator, dictValue)) + { + for (SysDictData dict : datas) + { + for (String value : dictValue.split(separator)) + { + if (value.equals(dict.getDictValue())) + { + propertyString.append(dict.getDictLabel()).append(separator); + break; + } + } + } + } + else + { + for (SysDictData dict : datas) + { + if (dictValue.equals(dict.getDictValue())) + { + return dict.getDictLabel(); + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 根据字典类型和字典标签获取字典值 + * + * @param dictType 字典类型 + * @param dictLabel 字典标签 + * @param separator 分隔符 + * @return 字典值 + */ + public static String getDictValue(String dictType, String dictLabel, String separator) + { + StringBuilder propertyString = new StringBuilder(); + List datas = getDictCache(dictType); + if (StringUtils.isNull(datas)) + { + return StringUtils.EMPTY; + } + if (StringUtils.containsAny(separator, dictLabel)) + { + for (SysDictData dict : datas) + { + for (String label : dictLabel.split(separator)) + { + if (label.equals(dict.getDictLabel())) + { + propertyString.append(dict.getDictValue()).append(separator); + break; + } + } + } + } + else + { + for (SysDictData dict : datas) + { + if (dictLabel.equals(dict.getDictLabel())) + { + return dict.getDictValue(); + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 根据字典类型获取字典所有值 + * + * @param dictType 字典类型 + * @return 字典值 + */ + public static String getDictValues(String dictType) + { + StringBuilder propertyString = new StringBuilder(); + List datas = getDictCache(dictType); + if (StringUtils.isNull(datas)) + { + return StringUtils.EMPTY; + } + for (SysDictData dict : datas) + { + propertyString.append(dict.getDictValue()).append(SEPARATOR); + } + return StringUtils.stripEnd(propertyString.toString(), SEPARATOR); + } + + /** + * 根据字典类型获取字典所有标签 + * + * @param dictType 字典类型 + * @return 字典值 + */ + public static String getDictLabels(String dictType) + { + StringBuilder propertyString = new StringBuilder(); + List datas = getDictCache(dictType); + if (StringUtils.isNull(datas)) + { + return StringUtils.EMPTY; + } + for (SysDictData dict : datas) + { + propertyString.append(dict.getDictLabel()).append(SEPARATOR); + } + return StringUtils.stripEnd(propertyString.toString(), SEPARATOR); + } + + /** + * 删除指定字典缓存 + * + * @param key 字典键 + */ + public static void removeDictCache(String key) + { + SpringUtils.getBean(RedisCache.class).deleteObject(getCacheKey(key)); + } + + /** + * 清空字典缓存 + */ + public static void clearDictCache() + { + Collection keys = SpringUtils.getBean(RedisCache.class).keys(CacheConstants.SYS_DICT_KEY + "*"); + SpringUtils.getBean(RedisCache.class).deleteObject(keys); + } + + /** + * 设置cache key + * + * @param configKey 参数键 + * @return 缓存键key + */ + public static String getCacheKey(String configKey) + { + return CacheConstants.SYS_DICT_KEY + configKey; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/ExceptionUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ExceptionUtil.java new file mode 100644 index 0000000..214e4a0 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ExceptionUtil.java @@ -0,0 +1,39 @@ +package com.ruoyi.common.utils; + +import java.io.PrintWriter; +import java.io.StringWriter; +import org.apache.commons.lang3.exception.ExceptionUtils; + +/** + * 错误信息处理类。 + * + * @author ruoyi + */ +public class ExceptionUtil +{ + /** + * 获取exception的详细错误信息。 + */ + public static String getExceptionMessage(Throwable e) + { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw, true)); + return sw.toString(); + } + + public static String getRootErrorMessage(Exception e) + { + Throwable root = ExceptionUtils.getRootCause(e); + root = (root == null ? e : root); + if (root == null) + { + return ""; + } + String msg = root.getMessage(); + if (msg == null) + { + return "null"; + } + return StringUtils.defaultString(msg); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/LogUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/LogUtils.java new file mode 100644 index 0000000..0de30c6 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/LogUtils.java @@ -0,0 +1,18 @@ +package com.ruoyi.common.utils; + +/** + * 处理并记录日志文件 + * + * @author ruoyi + */ +public class LogUtils +{ + public static String getBlock(Object msg) + { + if (msg == null) + { + msg = ""; + } + return "[" + msg.toString() + "]"; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/MessageUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/MessageUtils.java new file mode 100644 index 0000000..7dac75a --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/MessageUtils.java @@ -0,0 +1,26 @@ +package com.ruoyi.common.utils; + +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import com.ruoyi.common.utils.spring.SpringUtils; + +/** + * 获取i18n资源文件 + * + * @author ruoyi + */ +public class MessageUtils +{ + /** + * 根据消息键和参数 获取消息 委托给spring messageSource + * + * @param code 消息键 + * @param args 参数 + * @return 获取国际化翻译值 + */ + public static String message(String code, Object... args) + { + MessageSource messageSource = SpringUtils.getBean(MessageSource.class); + return messageSource.getMessage(code, args, LocaleContextHolder.getLocale()); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/PageUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/PageUtils.java new file mode 100644 index 0000000..70e9b08 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/PageUtils.java @@ -0,0 +1,35 @@ +package com.ruoyi.common.utils; + +import com.github.pagehelper.PageHelper; +import com.ruoyi.common.core.page.PageDomain; +import com.ruoyi.common.core.page.TableSupport; +import com.ruoyi.common.utils.sql.SqlUtil; + +/** + * 分页工具类 + * + * @author ruoyi + */ +public class PageUtils extends PageHelper +{ + /** + * 设置请求分页数据 + */ + public static void startPage() + { + PageDomain pageDomain = TableSupport.buildPageRequest(); + Integer pageNum = pageDomain.getPageNum(); + Integer pageSize = pageDomain.getPageSize(); + String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy()); + Boolean reasonable = pageDomain.getReasonable(); + PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable); + } + + /** + * 清理分页的线程变量 + */ + public static void clearPage() + { + PageHelper.clearPage(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/SecurityUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/SecurityUtils.java new file mode 100644 index 0000000..0d3ac5f --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/SecurityUtils.java @@ -0,0 +1,178 @@ +package com.ruoyi.common.utils; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.util.PatternMatchUtils; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.exception.ServiceException; + +/** + * 安全服务工具类 + * + * @author ruoyi + */ +public class SecurityUtils +{ + + /** + * 用户ID + **/ + public static Long getUserId() + { + try + { + return getLoginUser().getUserId(); + } + catch (Exception e) + { + throw new ServiceException("获取用户ID异常", HttpStatus.UNAUTHORIZED); + } + } + + /** + * 获取部门ID + **/ + public static Long getDeptId() + { + try + { + return getLoginUser().getDeptId(); + } + catch (Exception e) + { + throw new ServiceException("获取部门ID异常", HttpStatus.UNAUTHORIZED); + } + } + + /** + * 获取用户账户 + **/ + public static String getUsername() + { + try + { + return getLoginUser().getUsername(); + } + catch (Exception e) + { + throw new ServiceException("获取用户账户异常", HttpStatus.UNAUTHORIZED); + } + } + + /** + * 获取用户 + **/ + public static LoginUser getLoginUser() + { + try + { + return (LoginUser) getAuthentication().getPrincipal(); + } + catch (Exception e) + { + throw new ServiceException("获取用户信息异常", HttpStatus.UNAUTHORIZED); + } + } + + /** + * 获取Authentication + */ + public static Authentication getAuthentication() + { + return SecurityContextHolder.getContext().getAuthentication(); + } + + /** + * 生成BCryptPasswordEncoder密码 + * + * @param password 密码 + * @return 加密字符串 + */ + public static String encryptPassword(String password) + { + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + return passwordEncoder.encode(password); + } + + /** + * 判断密码是否相同 + * + * @param rawPassword 真实密码 + * @param encodedPassword 加密后字符 + * @return 结果 + */ + public static boolean matchesPassword(String rawPassword, String encodedPassword) + { + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + return passwordEncoder.matches(rawPassword, encodedPassword); + } + + /** + * 是否为管理员 + * + * @param userId 用户ID + * @return 结果 + */ + public static boolean isAdmin(Long userId) + { + return userId != null && 1L == userId; + } + + /** + * 验证用户是否具备某权限 + * + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + public static boolean hasPermi(String permission) + { + return hasPermi(getLoginUser().getPermissions(), permission); + } + + /** + * 判断是否包含权限 + * + * @param authorities 权限列表 + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + public static boolean hasPermi(Collection authorities, String permission) + { + return authorities.stream().filter(StringUtils::hasText) + .anyMatch(x -> Constants.ALL_PERMISSION.equals(x) || PatternMatchUtils.simpleMatch(x, permission)); + } + + /** + * 验证用户是否拥有某个角色 + * + * @param role 角色标识 + * @return 用户是否具备某角色 + */ + public static boolean hasRole(String role) + { + List roleList = getLoginUser().getUser().getRoles(); + Collection roles = roleList.stream().map(SysRole::getRoleKey).collect(Collectors.toSet()); + return hasRole(roles, role); + } + + /** + * 判断是否包含角色 + * + * @param roles 角色列表 + * @param role 角色 + * @return 用户是否具备某角色权限 + */ + public static boolean hasRole(Collection roles, String role) + { + return roles.stream().filter(StringUtils::hasText) + .anyMatch(x -> Constants.SUPER_ADMIN.equals(x) || PatternMatchUtils.simpleMatch(x, role)); + } + +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java new file mode 100644 index 0000000..febb603 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ServletUtils.java @@ -0,0 +1,218 @@ +package com.ruoyi.common.utils; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.text.Convert; + +/** + * 客户端工具类 + * + * @author ruoyi + */ +public class ServletUtils +{ + /** + * 获取String参数 + */ + public static String getParameter(String name) + { + return getRequest().getParameter(name); + } + + /** + * 获取String参数 + */ + public static String getParameter(String name, String defaultValue) + { + return Convert.toStr(getRequest().getParameter(name), defaultValue); + } + + /** + * 获取Integer参数 + */ + public static Integer getParameterToInt(String name) + { + return Convert.toInt(getRequest().getParameter(name)); + } + + /** + * 获取Integer参数 + */ + public static Integer getParameterToInt(String name, Integer defaultValue) + { + return Convert.toInt(getRequest().getParameter(name), defaultValue); + } + + /** + * 获取Boolean参数 + */ + public static Boolean getParameterToBool(String name) + { + return Convert.toBool(getRequest().getParameter(name)); + } + + /** + * 获取Boolean参数 + */ + public static Boolean getParameterToBool(String name, Boolean defaultValue) + { + return Convert.toBool(getRequest().getParameter(name), defaultValue); + } + + /** + * 获得所有请求参数 + * + * @param request 请求对象{@link ServletRequest} + * @return Map + */ + public static Map getParams(ServletRequest request) + { + final Map map = request.getParameterMap(); + return Collections.unmodifiableMap(map); + } + + /** + * 获得所有请求参数 + * + * @param request 请求对象{@link ServletRequest} + * @return Map + */ + public static Map getParamMap(ServletRequest request) + { + Map params = new HashMap<>(); + for (Map.Entry entry : getParams(request).entrySet()) + { + params.put(entry.getKey(), StringUtils.join(entry.getValue(), ",")); + } + return params; + } + + /** + * 获取request + */ + public static HttpServletRequest getRequest() + { + return getRequestAttributes().getRequest(); + } + + /** + * 获取response + */ + public static HttpServletResponse getResponse() + { + return getRequestAttributes().getResponse(); + } + + /** + * 获取session + */ + public static HttpSession getSession() + { + return getRequest().getSession(); + } + + public static ServletRequestAttributes getRequestAttributes() + { + RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); + return (ServletRequestAttributes) attributes; + } + + /** + * 将字符串渲染到客户端 + * + * @param response 渲染对象 + * @param string 待渲染的字符串 + */ + public static void renderString(HttpServletResponse response, String string) + { + try + { + response.setStatus(200); + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + response.getWriter().print(string); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + + /** + * 是否是Ajax异步请求 + * + * @param request + */ + public static boolean isAjaxRequest(HttpServletRequest request) + { + String accept = request.getHeader("accept"); + if (accept != null && accept.contains("application/json")) + { + return true; + } + + String xRequestedWith = request.getHeader("X-Requested-With"); + if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")) + { + return true; + } + + String uri = request.getRequestURI(); + if (StringUtils.inStringIgnoreCase(uri, ".json", ".xml")) + { + return true; + } + + String ajax = request.getParameter("__ajax"); + return StringUtils.inStringIgnoreCase(ajax, "json", "xml"); + } + + /** + * 内容编码 + * + * @param str 内容 + * @return 编码后的内容 + */ + public static String urlEncode(String str) + { + try + { + return URLEncoder.encode(str, Constants.UTF8); + } + catch (UnsupportedEncodingException e) + { + return StringUtils.EMPTY; + } + } + + /** + * 内容解码 + * + * @param str 内容 + * @return 解码后的内容 + */ + public static String urlDecode(String str) + { + try + { + return URLDecoder.decode(str, Constants.UTF8); + } + catch (UnsupportedEncodingException e) + { + return StringUtils.EMPTY; + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/StringUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/StringUtils.java new file mode 100644 index 0000000..c92759b --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/StringUtils.java @@ -0,0 +1,722 @@ +package com.ruoyi.common.utils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.springframework.util.AntPathMatcher; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.text.StrFormatter; + +/** + * 字符串工具类 + * + * @author ruoyi + */ +public class StringUtils extends org.apache.commons.lang3.StringUtils +{ + /** 空字符串 */ + private static final String NULLSTR = ""; + + /** 下划线 */ + private static final char SEPARATOR = '_'; + + /** 星号 */ + private static final char ASTERISK = '*'; + + /** + * 获取参数不为空值 + * + * @param value defaultValue 要判断的value + * @return value 返回值 + */ + public static T nvl(T value, T defaultValue) + { + return value != null ? value : defaultValue; + } + + /** + * * 判断一个Collection是否为空, 包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Collection coll) + { + return isNull(coll) || coll.isEmpty(); + } + + /** + * * 判断一个Collection是否非空,包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Collection coll) + { + return !isEmpty(coll); + } + + /** + * * 判断一个对象数组是否为空 + * + * @param objects 要判断的对象数组 + ** @return true:为空 false:非空 + */ + public static boolean isEmpty(Object[] objects) + { + return isNull(objects) || (objects.length == 0); + } + + /** + * * 判断一个对象数组是否非空 + * + * @param objects 要判断的对象数组 + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Object[] objects) + { + return !isEmpty(objects); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Map map) + { + return isNull(map) || map.isEmpty(); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Map map) + { + return !isEmpty(map); + } + + /** + * * 判断一个字符串是否为空串 + * + * @param str String + * @return true:为空 false:非空 + */ + public static boolean isEmpty(String str) + { + return isNull(str) || NULLSTR.equals(str.trim()); + } + + /** + * * 判断一个字符串是否为非空串 + * + * @param str String + * @return true:非空串 false:空串 + */ + public static boolean isNotEmpty(String str) + { + return !isEmpty(str); + } + + /** + * * 判断一个对象是否为空 + * + * @param object Object + * @return true:为空 false:非空 + */ + public static boolean isNull(Object object) + { + return object == null; + } + + /** + * * 判断一个对象是否非空 + * + * @param object Object + * @return true:非空 false:空 + */ + public static boolean isNotNull(Object object) + { + return !isNull(object); + } + + /** + * * 判断一个对象是否是数组类型(Java基本型别的数组) + * + * @param object 对象 + * @return true:是数组 false:不是数组 + */ + public static boolean isArray(Object object) + { + return isNotNull(object) && object.getClass().isArray(); + } + + /** + * 去空格 + */ + public static String trim(String str) + { + return (str == null ? "" : str.trim()); + } + + /** + * 替换指定字符串的指定区间内字符为"*" + * + * @param str 字符串 + * @param startInclude 开始位置(包含) + * @param endExclude 结束位置(不包含) + * @return 替换后的字符串 + */ + public static String hide(CharSequence str, int startInclude, int endExclude) + { + if (isEmpty(str)) + { + return NULLSTR; + } + final int strLength = str.length(); + if (startInclude > strLength) + { + return NULLSTR; + } + if (endExclude > strLength) + { + endExclude = strLength; + } + if (startInclude > endExclude) + { + // 如果起始位置大于结束位置,不替换 + return NULLSTR; + } + final char[] chars = new char[strLength]; + for (int i = 0; i < strLength; i++) + { + if (i >= startInclude && i < endExclude) + { + chars[i] = ASTERISK; + } + else + { + chars[i] = str.charAt(i); + } + } + return new String(chars); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @return 结果 + */ + public static String substring(final String str, int start) + { + if (str == null) + { + return NULLSTR; + } + + if (start < 0) + { + start = str.length() + start; + } + + if (start < 0) + { + start = 0; + } + if (start > str.length()) + { + return NULLSTR; + } + + return str.substring(start); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @param end 结束 + * @return 结果 + */ + public static String substring(final String str, int start, int end) + { + if (str == null) + { + return NULLSTR; + } + + if (end < 0) + { + end = str.length() + end; + } + if (start < 0) + { + start = str.length() + start; + } + + if (end > str.length()) + { + end = str.length(); + } + + if (start > end) + { + return NULLSTR; + } + + if (start < 0) + { + start = 0; + } + if (end < 0) + { + end = 0; + } + + return str.substring(start, end); + } + + /** + * 在字符串中查找第一个出现的 `open` 和最后一个出现的 `close` 之间的子字符串 + * + * @param str 要截取的字符串 + * @param open 起始字符串 + * @param close 结束字符串 + * @return 截取结果 + */ + public static String substringBetweenLast(final String str, final String open, final String close) + { + if (isEmpty(str) || isEmpty(open) || isEmpty(close)) + { + return NULLSTR; + } + final int start = str.indexOf(open); + if (start != INDEX_NOT_FOUND) + { + final int end = str.lastIndexOf(close); + if (end != INDEX_NOT_FOUND) + { + return str.substring(start + open.length(), end); + } + } + return NULLSTR; + } + + /** + * 判断是否为空,并且不是空白字符 + * + * @param str 要判断的value + * @return 结果 + */ + public static boolean hasText(String str) + { + return (str != null && !str.isEmpty() && containsText(str)); + } + + private static boolean containsText(CharSequence str) + { + int strLen = str.length(); + for (int i = 0; i < strLen; i++) + { + if (!Character.isWhitespace(str.charAt(i))) + { + return true; + } + } + return false; + } + + /** + * 格式化文本, {} 表示占位符
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param template 文本模板,被替换的部分用 {} 表示 + * @param params 参数值 + * @return 格式化后的文本 + */ + public static String format(String template, Object... params) + { + if (isEmpty(params) || isEmpty(template)) + { + return template; + } + return StrFormatter.format(template, params); + } + + /** + * 是否为http(s)://开头 + * + * @param link 链接 + * @return 结果 + */ + public static boolean ishttp(String link) + { + return StringUtils.startsWithAny(link, Constants.HTTP, Constants.HTTPS); + } + + /** + * 字符串转set + * + * @param str 字符串 + * @param sep 分隔符 + * @return set集合 + */ + public static final Set str2Set(String str, String sep) + { + return new HashSet(str2List(str, sep, true, false)); + } + + /** + * 字符串转list + * + * @param str 字符串 + * @param sep 分隔符 + * @return list集合 + */ + public static final List str2List(String str, String sep) + { + return str2List(str, sep, true, false); + } + + /** + * 字符串转list + * + * @param str 字符串 + * @param sep 分隔符 + * @param filterBlank 过滤纯空白 + * @param trim 去掉首尾空白 + * @return list集合 + */ + public static final List str2List(String str, String sep, boolean filterBlank, boolean trim) + { + List list = new ArrayList(); + if (StringUtils.isEmpty(str)) + { + return list; + } + + // 过滤空白字符串 + if (filterBlank && StringUtils.isBlank(str)) + { + return list; + } + String[] split = str.split(sep); + for (String string : split) + { + if (filterBlank && StringUtils.isBlank(string)) + { + continue; + } + if (trim) + { + string = string.trim(); + } + list.add(string); + } + + return list; + } + + /** + * 判断给定的collection列表中是否包含数组array 判断给定的数组array中是否包含给定的元素value + * + * @param collection 给定的集合 + * @param array 给定的数组 + * @return boolean 结果 + */ + public static boolean containsAny(Collection collection, String... array) + { + if (isEmpty(collection) || isEmpty(array)) + { + return false; + } + else + { + for (String str : array) + { + if (collection.contains(str)) + { + return true; + } + } + return false; + } + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写 + * + * @param cs 指定字符串 + * @param searchCharSequences 需要检查的字符串数组 + * @return 是否包含任意一个字符串 + */ + public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences) + { + if (isEmpty(cs) || isEmpty(searchCharSequences)) + { + return false; + } + for (CharSequence testStr : searchCharSequences) + { + if (containsIgnoreCase(cs, testStr)) + { + return true; + } + } + return false; + } + + /** + * 驼峰转下划线命名 + */ + public static String toUnderScoreCase(String str) + { + if (str == null) + { + return null; + } + StringBuilder sb = new StringBuilder(); + // 前置字符是否大写 + boolean preCharIsUpperCase = true; + // 当前字符是否大写 + boolean curreCharIsUpperCase = true; + // 下一字符是否大写 + boolean nexteCharIsUpperCase = true; + for (int i = 0; i < str.length(); i++) + { + char c = str.charAt(i); + if (i > 0) + { + preCharIsUpperCase = Character.isUpperCase(str.charAt(i - 1)); + } + else + { + preCharIsUpperCase = false; + } + + curreCharIsUpperCase = Character.isUpperCase(c); + + if (i < (str.length() - 1)) + { + nexteCharIsUpperCase = Character.isUpperCase(str.charAt(i + 1)); + } + + if (preCharIsUpperCase && curreCharIsUpperCase && !nexteCharIsUpperCase) + { + sb.append(SEPARATOR); + } + else if ((i != 0 && !preCharIsUpperCase) && curreCharIsUpperCase) + { + sb.append(SEPARATOR); + } + sb.append(Character.toLowerCase(c)); + } + + return sb.toString(); + } + + /** + * 是否包含字符串 + * + * @param str 验证字符串 + * @param strs 字符串组 + * @return 包含返回true + */ + public static boolean inStringIgnoreCase(String str, String... strs) + { + if (str != null && strs != null) + { + for (String s : strs) + { + if (str.equalsIgnoreCase(trim(s))) + { + return true; + } + } + } + return false; + } + + /** + * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld + * + * @param name 转换前的下划线大写方式命名的字符串 + * @return 转换后的驼峰式命名的字符串 + */ + public static String convertToCamelCase(String name) + { + StringBuilder result = new StringBuilder(); + // 快速检查 + if (name == null || name.isEmpty()) + { + // 没必要转换 + return ""; + } + else if (!name.contains("_")) + { + // 不含下划线,仅将首字母大写 + return name.substring(0, 1).toUpperCase() + name.substring(1); + } + // 用下划线将原始字符串分割 + String[] camels = name.split("_"); + for (String camel : camels) + { + // 跳过原始字符串中开头、结尾的下换线或双重下划线 + if (camel.isEmpty()) + { + continue; + } + // 首字母大写 + result.append(camel.substring(0, 1).toUpperCase()); + result.append(camel.substring(1).toLowerCase()); + } + return result.toString(); + } + + /** + * 驼峰式命名法 + * 例如:user_name->userName + */ + public static String toCamelCase(String s) + { + if (s == null) + { + return null; + } + if (s.indexOf(SEPARATOR) == -1) + { + return s; + } + 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(); + } + + /** + * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串 + * + * @param str 指定字符串 + * @param strs 需要检查的字符串数组 + * @return 是否匹配 + */ + public static boolean matches(String str, List strs) + { + if (isEmpty(str) || isEmpty(strs)) + { + return false; + } + for (String pattern : strs) + { + if (isMatch(pattern, str)) + { + return true; + } + } + return false; + } + + /** + * 判断url是否与规则配置: + * ? 表示单个字符; + * * 表示一层路径内的任意字符串,不可跨层级; + * ** 表示任意层路径; + * + * @param pattern 匹配规则 + * @param url 需要匹配的url + * @return + */ + public static boolean isMatch(String pattern, String url) + { + AntPathMatcher matcher = new AntPathMatcher(); + return matcher.match(pattern, url); + } + + @SuppressWarnings("unchecked") + public static T cast(Object obj) + { + return (T) obj; + } + + /** + * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。 + * + * @param num 数字对象 + * @param size 字符串指定长度 + * @return 返回数字的字符串格式,该字符串为指定长度。 + */ + public static final String padl(final Number num, final int size) + { + return padl(num.toString(), size, '0'); + } + + /** + * 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。 + * + * @param s 原始字符串 + * @param size 字符串指定长度 + * @param c 用于补齐的字符 + * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。 + */ + public static final String padl(final String s, final int size, final char c) + { + final StringBuilder sb = new StringBuilder(size); + if (s != null) + { + final int len = s.length(); + if (s.length() <= size) + { + for (int i = size - len; i > 0; i--) + { + sb.append(c); + } + sb.append(s); + } + else + { + return s.substring(len - size, len); + } + } + else + { + for (int i = size; i > 0; i--) + { + sb.append(c); + } + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/Threads.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/Threads.java new file mode 100644 index 0000000..71fe6d5 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/Threads.java @@ -0,0 +1,99 @@ +package com.ruoyi.common.utils; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 线程相关工具类. + * + * @author ruoyi + */ +public class Threads +{ + private static final Logger logger = LoggerFactory.getLogger(Threads.class); + + /** + * sleep等待,单位为毫秒 + */ + public static void sleep(long milliseconds) + { + try + { + Thread.sleep(milliseconds); + } + catch (InterruptedException e) + { + return; + } + } + + /** + * 停止线程池 + * 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务. + * 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数. + * 如果仍然超時,則強制退出. + * 另对在shutdown时线程本身被调用中断做了处理. + */ + public static void shutdownAndAwaitTermination(ExecutorService pool) + { + if (pool != null && !pool.isShutdown()) + { + pool.shutdown(); + try + { + if (!pool.awaitTermination(120, TimeUnit.SECONDS)) + { + pool.shutdownNow(); + if (!pool.awaitTermination(120, TimeUnit.SECONDS)) + { + logger.info("Pool did not terminate"); + } + } + } + catch (InterruptedException ie) + { + pool.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } + + /** + * 打印线程异常信息 + */ + public static void printException(Runnable r, Throwable t) + { + if (t == null && r instanceof Future) + { + try + { + Future future = (Future) r; + if (future.isDone()) + { + future.get(); + } + } + catch (CancellationException ce) + { + t = ce; + } + catch (ExecutionException ee) + { + t = ee.getCause(); + } + catch (InterruptedException ie) + { + Thread.currentThread().interrupt(); + } + } + if (t != null) + { + logger.error(t.getMessage(), t); + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanUtils.java new file mode 100644 index 0000000..4463662 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanUtils.java @@ -0,0 +1,110 @@ +package com.ruoyi.common.utils.bean; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Bean 工具类 + * + * @author ruoyi + */ +public class BeanUtils extends org.springframework.beans.BeanUtils +{ + /** Bean方法名中属性名开始的下标 */ + private static final int BEAN_METHOD_PROP_INDEX = 3; + + /** * 匹配getter方法的正则表达式 */ + private static final Pattern GET_PATTERN = Pattern.compile("get(\\p{javaUpperCase}\\w*)"); + + /** * 匹配setter方法的正则表达式 */ + private static final Pattern SET_PATTERN = Pattern.compile("set(\\p{javaUpperCase}\\w*)"); + + /** + * Bean属性复制工具方法。 + * + * @param dest 目标对象 + * @param src 源对象 + */ + public static void copyBeanProp(Object dest, Object src) + { + try + { + copyProperties(src, dest); + } + catch (Exception e) + { + e.printStackTrace(); + } + } + + /** + * 获取对象的setter方法。 + * + * @param obj 对象 + * @return 对象的setter方法列表 + */ + public static List getSetterMethods(Object obj) + { + // setter方法列表 + List setterMethods = new ArrayList(); + + // 获取所有方法 + Method[] methods = obj.getClass().getMethods(); + + // 查找setter方法 + + for (Method method : methods) + { + Matcher m = SET_PATTERN.matcher(method.getName()); + if (m.matches() && (method.getParameterTypes().length == 1)) + { + setterMethods.add(method); + } + } + // 返回setter方法列表 + return setterMethods; + } + + /** + * 获取对象的getter方法。 + * + * @param obj 对象 + * @return 对象的getter方法列表 + */ + + public static List getGetterMethods(Object obj) + { + // getter方法列表 + List getterMethods = new ArrayList(); + // 获取所有方法 + Method[] methods = obj.getClass().getMethods(); + // 查找getter方法 + for (Method method : methods) + { + Matcher m = GET_PATTERN.matcher(method.getName()); + if (m.matches() && (method.getParameterTypes().length == 0)) + { + getterMethods.add(method); + } + } + // 返回getter方法列表 + return getterMethods; + } + + /** + * 检查Bean方法名中的属性名是否相等。
+ * 如getName()和setName()属性名一样,getName()和setAge()属性名不一样。 + * + * @param m1 方法名1 + * @param m2 方法名2 + * @return 属性名一样返回true,否则返回false + */ + + public static boolean isMethodPropEquals(String m1, String m2) + { + return m1.substring(BEAN_METHOD_PROP_INDEX).equals(m2.substring(BEAN_METHOD_PROP_INDEX)); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanValidators.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanValidators.java new file mode 100644 index 0000000..80bfed7 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/bean/BeanValidators.java @@ -0,0 +1,24 @@ +package com.ruoyi.common.utils.bean; + +import java.util.Set; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validator; + +/** + * bean对象属性验证 + * + * @author ruoyi + */ +public class BeanValidators +{ + public static void validateWithException(Validator validator, Object object, Class... groups) + throws ConstraintViolationException + { + Set> constraintViolations = validator.validate(object, groups); + if (!constraintViolations.isEmpty()) + { + throw new ConstraintViolationException(constraintViolations); + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileTypeUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileTypeUtils.java new file mode 100644 index 0000000..68130b9 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileTypeUtils.java @@ -0,0 +1,76 @@ +package com.ruoyi.common.utils.file; + +import java.io.File; +import org.apache.commons.lang3.StringUtils; + +/** + * 文件类型工具类 + * + * @author ruoyi + */ +public class FileTypeUtils +{ + /** + * 获取文件类型 + *

+ * 例如: ruoyi.txt, 返回: txt + * + * @param file 文件名 + * @return 后缀(不含".") + */ + public static String getFileType(File file) + { + if (null == file) + { + return StringUtils.EMPTY; + } + return getFileType(file.getName()); + } + + /** + * 获取文件类型 + *

+ * 例如: ruoyi.txt, 返回: txt + * + * @param fileName 文件名 + * @return 后缀(不含".") + */ + public static String getFileType(String fileName) + { + int separatorIndex = fileName.lastIndexOf("."); + if (separatorIndex < 0) + { + return ""; + } + return fileName.substring(separatorIndex + 1).toLowerCase(); + } + + /** + * 获取文件类型 + * + * @param photoByte 文件字节码 + * @return 后缀(不含".") + */ + public static String getFileExtendName(byte[] photoByte) + { + String strFileExtendName = "JPG"; + if ((photoByte[0] == 71) && (photoByte[1] == 73) && (photoByte[2] == 70) && (photoByte[3] == 56) + && ((photoByte[4] == 55) || (photoByte[4] == 57)) && (photoByte[5] == 97)) + { + strFileExtendName = "GIF"; + } + else if ((photoByte[6] == 74) && (photoByte[7] == 70) && (photoByte[8] == 73) && (photoByte[9] == 70)) + { + strFileExtendName = "JPG"; + } + else if ((photoByte[0] == 66) && (photoByte[1] == 77)) + { + strFileExtendName = "BMP"; + } + else if ((photoByte[1] == 80) && (photoByte[2] == 78) && (photoByte[3] == 71)) + { + strFileExtendName = "PNG"; + } + return strFileExtendName; + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java new file mode 100644 index 0000000..cc1b6f4 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java @@ -0,0 +1,260 @@ +package com.ruoyi.common.utils.file; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.Objects; +import org.apache.commons.io.FilenameUtils; +import org.springframework.web.multipart.MultipartFile; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.exception.file.FileNameLengthLimitExceededException; +import com.ruoyi.common.exception.file.FileSizeLimitExceededException; +import com.ruoyi.common.exception.file.InvalidExtensionException; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.uuid.IdUtils; +import com.ruoyi.common.utils.uuid.Seq; + +/** + * 文件上传工具类 + * + * @author ruoyi + */ +public class FileUploadUtils +{ + /** + * 默认大小 50M + */ + public static final long DEFAULT_MAX_SIZE = 50 * 1024 * 1024L; + + /** + * 默认的文件名最大长度 100 + */ + public static final int DEFAULT_FILE_NAME_LENGTH = 100; + + /** + * 默认上传的地址 + */ + private static String defaultBaseDir = RuoYiConfig.getProfile(); + + public static void setDefaultBaseDir(String defaultBaseDir) + { + FileUploadUtils.defaultBaseDir = defaultBaseDir; + } + + public static String getDefaultBaseDir() + { + return defaultBaseDir; + } + + /** + * 以默认配置进行文件上传 + * + * @param file 上传的文件 + * @return 文件名称 + * @throws Exception + */ + public static final String upload(MultipartFile file) throws IOException + { + try + { + return upload(getDefaultBaseDir(), file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); + } + catch (Exception e) + { + throw new IOException(e.getMessage(), e); + } + } + + /** + * 根据文件路径上传 + * + * @param baseDir 相对应用的基目录 + * @param file 上传的文件 + * @return 文件名称 + * @throws IOException + */ + public static final String upload(String baseDir, MultipartFile file) throws IOException + { + try + { + return upload(baseDir, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); + } + catch (Exception e) + { + throw new IOException(e.getMessage(), e); + } + } + + /** + * 文件上传 + * + * @param baseDir 相对应用的基目录 + * @param file 上传的文件 + * @param allowedExtension 上传文件类型 + * @return 返回上传成功的文件名 + * @throws FileSizeLimitExceededException 如果超出最大大小 + * @throws FileNameLengthLimitExceededException 文件名太长 + * @throws IOException 比如读写文件出错时 + * @throws InvalidExtensionException 文件校验异常 + */ + public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension) + throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException, + InvalidExtensionException + { + return upload(baseDir, file, allowedExtension, false); + } + + /** + * 文件上传 + * + * @param baseDir 相对应用的基目录 + * @param file 上传的文件 + * @param useCustomNaming 系统自定义文件名 + * @param allowedExtension 上传文件类型 + * @return 返回上传成功的文件名 + * @throws FileSizeLimitExceededException 如果超出最大大小 + * @throws FileNameLengthLimitExceededException 文件名太长 + * @throws IOException 比如读写文件出错时 + * @throws InvalidExtensionException 文件校验异常 + */ + public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension, boolean useCustomNaming) + throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException, + InvalidExtensionException + { + int fileNameLength = Objects.requireNonNull(file.getOriginalFilename()).length(); + if (fileNameLength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH) + { + throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH); + } + + assertAllowed(file, allowedExtension); + + String fileName = useCustomNaming ? uuidFilename(file) : extractFilename(file); + + String absPath = getAbsoluteFile(baseDir, fileName).getAbsolutePath(); + file.transferTo(Paths.get(absPath)); + return getPathFileName(baseDir, fileName); + } + + /** + * 编码文件名(日期格式目录 + 原文件名 + 序列值 + 后缀) + */ + public static final String extractFilename(MultipartFile file) + { + return StringUtils.format("{}/{}_{}.{}", DateUtils.datePath(), FilenameUtils.getBaseName(file.getOriginalFilename()), Seq.getId(Seq.uploadSeqType), getExtension(file)); + } + + /** + * 编编码文件名(日期格式目录 + UUID + 后缀) + */ + public static final String uuidFilename(MultipartFile file) + { + return StringUtils.format("{}/{}.{}", DateUtils.datePath(), IdUtils.fastSimpleUUID(), getExtension(file)); + } + + public static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException + { + File desc = new File(uploadDir + File.separator + fileName); + + if (!desc.exists()) + { + if (!desc.getParentFile().exists()) + { + desc.getParentFile().mkdirs(); + } + } + return desc; + } + + public static final String getPathFileName(String uploadDir, String fileName) throws IOException + { + int dirLastIndex = RuoYiConfig.getProfile().length() + 1; + String currentDir = StringUtils.substring(uploadDir, dirLastIndex); + return Constants.RESOURCE_PREFIX + "/" + currentDir + "/" + fileName; + } + + /** + * 文件大小校验 + * + * @param file 上传的文件 + * @return + * @throws FileSizeLimitExceededException 如果超出最大大小 + * @throws InvalidExtensionException + */ + public static final void assertAllowed(MultipartFile file, String[] allowedExtension) + throws FileSizeLimitExceededException, InvalidExtensionException + { + long size = file.getSize(); + if (size > DEFAULT_MAX_SIZE) + { + throw new FileSizeLimitExceededException(DEFAULT_MAX_SIZE / 1024 / 1024); + } + + String fileName = file.getOriginalFilename(); + String extension = getExtension(file); + if (allowedExtension != null && !isAllowedExtension(extension, allowedExtension)) + { + if (allowedExtension == MimeTypeUtils.IMAGE_EXTENSION) + { + throw new InvalidExtensionException.InvalidImageExtensionException(allowedExtension, extension, + fileName); + } + else if (allowedExtension == MimeTypeUtils.FLASH_EXTENSION) + { + throw new InvalidExtensionException.InvalidFlashExtensionException(allowedExtension, extension, + fileName); + } + else if (allowedExtension == MimeTypeUtils.MEDIA_EXTENSION) + { + throw new InvalidExtensionException.InvalidMediaExtensionException(allowedExtension, extension, + fileName); + } + else if (allowedExtension == MimeTypeUtils.VIDEO_EXTENSION) + { + throw new InvalidExtensionException.InvalidVideoExtensionException(allowedExtension, extension, + fileName); + } + else + { + throw new InvalidExtensionException(allowedExtension, extension, fileName); + } + } + } + + /** + * 判断MIME类型是否是允许的MIME类型 + * + * @param extension + * @param allowedExtension + * @return + */ + public static final boolean isAllowedExtension(String extension, String[] allowedExtension) + { + for (String str : allowedExtension) + { + if (str.equalsIgnoreCase(extension)) + { + return true; + } + } + return false; + } + + /** + * 获取文件名的后缀 + * + * @param file 表单文件 + * @return 后缀名 + */ + public static final String getExtension(MultipartFile file) + { + String extension = FilenameUtils.getExtension(file.getOriginalFilename()); + if (StringUtils.isEmpty(extension)) + { + extension = MimeTypeUtils.getExtension(Objects.requireNonNull(file.getContentType())); + } + return extension; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUtils.java new file mode 100644 index 0000000..1f27265 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUtils.java @@ -0,0 +1,303 @@ +package com.ruoyi.common.utils.file; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.ArrayUtils; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.uuid.IdUtils; + +/** + * 文件处理工具类 + * + * @author ruoyi + */ +public class FileUtils +{ + public static String FILENAME_PATTERN = "[a-zA-Z0-9_\\-\\|\\.\\u4e00-\\u9fa5]+"; + + /** + * 输出指定文件的byte数组 + * + * @param filePath 文件路径 + * @param os 输出流 + * @return + */ + public static void writeBytes(String filePath, OutputStream os) throws IOException + { + FileInputStream fis = null; + try + { + File file = new File(filePath); + if (!file.exists()) + { + throw new FileNotFoundException(filePath); + } + fis = new FileInputStream(file); + byte[] b = new byte[1024]; + int length; + while ((length = fis.read(b)) > 0) + { + os.write(b, 0, length); + } + } + catch (IOException e) + { + throw e; + } + finally + { + IOUtils.close(os); + IOUtils.close(fis); + } + } + + /** + * 写数据到文件中 + * + * @param data 数据 + * @return 目标文件 + * @throws IOException IO异常 + */ + public static String writeImportBytes(byte[] data) throws IOException + { + return writeBytes(data, RuoYiConfig.getImportPath()); + } + + /** + * 写数据到文件中 + * + * @param data 数据 + * @param uploadDir 目标文件 + * @return 目标文件 + * @throws IOException IO异常 + */ + public static String writeBytes(byte[] data, String uploadDir) throws IOException + { + FileOutputStream fos = null; + String pathName = ""; + try + { + String extension = getFileExtendName(data); + pathName = DateUtils.datePath() + "/" + IdUtils.fastUUID() + "." + extension; + File file = FileUploadUtils.getAbsoluteFile(uploadDir, pathName); + fos = new FileOutputStream(file); + fos.write(data); + } + finally + { + IOUtils.close(fos); + } + return FileUploadUtils.getPathFileName(uploadDir, pathName); + } + + /** + * 移除路径中的请求前缀片段 + * + * @param filePath 文件路径 + * @return 移除后的文件路径 + */ + public static String stripPrefix(String filePath) + { + return StringUtils.substringAfter(filePath, Constants.RESOURCE_PREFIX); + } + + /** + * 删除文件 + * + * @param filePath 文件 + * @return + */ + public static boolean deleteFile(String filePath) + { + boolean flag = false; + File file = new File(filePath); + // 路径为文件且不为空则进行删除 + if (file.isFile() && file.exists()) + { + flag = file.delete(); + } + return flag; + } + + /** + * 文件名称验证 + * + * @param filename 文件名称 + * @return true 正常 false 非法 + */ + public static boolean isValidFilename(String filename) + { + return filename.matches(FILENAME_PATTERN); + } + + /** + * 检查文件是否可下载 + * + * @param resource 需要下载的文件 + * @return true 正常 false 非法 + */ + public static boolean checkAllowDownload(String resource) + { + // 禁止目录上跳级别 + if (StringUtils.contains(resource, "..")) + { + return false; + } + + // 检查允许下载的文件规则 + if (ArrayUtils.contains(MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION, FileTypeUtils.getFileType(resource))) + { + return true; + } + + // 不在允许下载的文件规则 + return false; + } + + /** + * 下载文件名重新编码 + * + * @param request 请求对象 + * @param fileName 文件名 + * @return 编码后的文件名 + */ + public static String setFileDownloadHeader(HttpServletRequest request, String fileName) throws UnsupportedEncodingException + { + final String agent = request.getHeader("USER-AGENT"); + String filename = fileName; + if (agent.contains("MSIE")) + { + // IE浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + filename = filename.replace("+", " "); + } + else if (agent.contains("Firefox")) + { + // 火狐浏览器 + filename = new String(fileName.getBytes(), "ISO8859-1"); + } + else if (agent.contains("Chrome")) + { + // google浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + } + else + { + // 其它浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + } + return filename; + } + + /** + * 下载文件名重新编码 + * + * @param response 响应对象 + * @param realFileName 真实文件名 + */ + public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) throws UnsupportedEncodingException + { + String percentEncodedFileName = percentEncode(realFileName); + + StringBuilder contentDispositionValue = new StringBuilder(); + contentDispositionValue.append("attachment; filename=") + .append(percentEncodedFileName) + .append(";") + .append("filename*=") + .append("utf-8''") + .append(percentEncodedFileName); + + response.addHeader("Access-Control-Expose-Headers", "Content-Disposition,download-filename"); + response.setHeader("Content-disposition", contentDispositionValue.toString()); + response.setHeader("download-filename", percentEncodedFileName); + } + + /** + * 百分号编码工具方法 + * + * @param s 需要百分号编码的字符串 + * @return 百分号编码后的字符串 + */ + public static String percentEncode(String s) throws UnsupportedEncodingException + { + String encode = URLEncoder.encode(s, StandardCharsets.UTF_8.toString()); + return encode.replaceAll("\\+", "%20"); + } + + /** + * 获取图像后缀 + * + * @param photoByte 图像数据 + * @return 后缀名 + */ + public static String getFileExtendName(byte[] photoByte) + { + String strFileExtendName = "jpg"; + if ((photoByte[0] == 71) && (photoByte[1] == 73) && (photoByte[2] == 70) && (photoByte[3] == 56) + && ((photoByte[4] == 55) || (photoByte[4] == 57)) && (photoByte[5] == 97)) + { + strFileExtendName = "gif"; + } + else if ((photoByte[6] == 74) && (photoByte[7] == 70) && (photoByte[8] == 73) && (photoByte[9] == 70)) + { + strFileExtendName = "jpg"; + } + else if ((photoByte[0] == 66) && (photoByte[1] == 77)) + { + strFileExtendName = "bmp"; + } + else if ((photoByte[1] == 80) && (photoByte[2] == 78) && (photoByte[3] == 71)) + { + strFileExtendName = "png"; + } + return strFileExtendName; + } + + /** + * 获取文件名称 /profile/upload/2022/04/16/ruoyi.png -- ruoyi.png + * + * @param fileName 路径名称 + * @return 没有文件路径的名称 + */ + public static String getName(String fileName) + { + if (fileName == null) + { + return null; + } + int lastUnixPos = fileName.lastIndexOf('/'); + int lastWindowsPos = fileName.lastIndexOf('\\'); + int index = Math.max(lastUnixPos, lastWindowsPos); + return fileName.substring(index + 1); + } + + /** + * 获取不带后缀文件名称 /profile/upload/2022/04/16/ruoyi.png -- ruoyi + * + * @param fileName 路径名称 + * @return 没有文件路径和后缀的名称 + */ + public static String getNameNotSuffix(String fileName) + { + if (fileName == null) + { + return null; + } + String baseName = FilenameUtils.getBaseName(fileName); + return baseName; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/ImageUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/ImageUtils.java new file mode 100644 index 0000000..432dfda --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/ImageUtils.java @@ -0,0 +1,98 @@ +package com.ruoyi.common.utils.file; + +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.Arrays; +import org.apache.poi.util.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.utils.StringUtils; + +/** + * 图片处理工具类 + * + * @author ruoyi + */ +public class ImageUtils +{ + private static final Logger log = LoggerFactory.getLogger(ImageUtils.class); + + public static byte[] getImage(String imagePath) + { + InputStream is = getFile(imagePath); + try + { + return IOUtils.toByteArray(is); + } + catch (Exception e) + { + log.error("图片加载异常 {}", e); + return null; + } + finally + { + IOUtils.closeQuietly(is); + } + } + + public static InputStream getFile(String imagePath) + { + try + { + byte[] result = readFile(imagePath); + result = Arrays.copyOf(result, result.length); + return new ByteArrayInputStream(result); + } + catch (Exception e) + { + log.error("获取图片异常 {}", e); + } + return null; + } + + /** + * 读取文件为字节数据 + * + * @param url 地址 + * @return 字节数据 + */ + public static byte[] readFile(String url) + { + InputStream in = null; + try + { + if (url.startsWith("http")) + { + // 网络地址 + URL urlObj = new URL(url); + URLConnection urlConnection = urlObj.openConnection(); + urlConnection.setConnectTimeout(30 * 1000); + urlConnection.setReadTimeout(60 * 1000); + urlConnection.setDoInput(true); + in = urlConnection.getInputStream(); + } + else + { + // 本机地址 + String localPath = RuoYiConfig.getProfile(); + String downloadPath = localPath + StringUtils.substringAfter(url, Constants.RESOURCE_PREFIX); + in = new FileInputStream(downloadPath); + } + return IOUtils.toByteArray(in); + } + catch (Exception e) + { + log.error("获取文件路径异常 {}", e); + return null; + } + finally + { + IOUtils.closeQuietly(in); + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/MimeTypeUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/MimeTypeUtils.java new file mode 100644 index 0000000..f968f1a --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/MimeTypeUtils.java @@ -0,0 +1,59 @@ +package com.ruoyi.common.utils.file; + +/** + * 媒体类型工具类 + * + * @author ruoyi + */ +public class MimeTypeUtils +{ + public static final String IMAGE_PNG = "image/png"; + + public static final String IMAGE_JPG = "image/jpg"; + + public static final String IMAGE_JPEG = "image/jpeg"; + + public static final String IMAGE_BMP = "image/bmp"; + + public static final String IMAGE_GIF = "image/gif"; + + public static final String[] IMAGE_EXTENSION = { "bmp", "gif", "jpg", "jpeg", "png" }; + + public static final String[] FLASH_EXTENSION = { "swf", "flv" }; + + public static final String[] MEDIA_EXTENSION = { "swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg", + "asf", "rm", "rmvb" }; + + public static final String[] VIDEO_EXTENSION = { "mp4", "avi", "rmvb" }; + + public static final String[] DEFAULT_ALLOWED_EXTENSION = { + // 图片 + "bmp", "gif", "jpg", "jpeg", "png", + // word excel powerpoint + "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt", + // 压缩文件 + "rar", "zip", "gz", "bz2", + // 视频格式 + "mp4", "avi", "rmvb", + // pdf + "pdf" }; + + public static String getExtension(String prefix) + { + switch (prefix) + { + case IMAGE_PNG: + return "png"; + case IMAGE_JPG: + return "jpg"; + case IMAGE_JPEG: + return "jpeg"; + case IMAGE_BMP: + return "bmp"; + case IMAGE_GIF: + return "gif"; + default: + return ""; + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/EscapeUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/EscapeUtil.java new file mode 100644 index 0000000..f52e83e --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/EscapeUtil.java @@ -0,0 +1,167 @@ +package com.ruoyi.common.utils.html; + +import com.ruoyi.common.utils.StringUtils; + +/** + * 转义和反转义工具类 + * + * @author ruoyi + */ +public class EscapeUtil +{ + public static final String RE_HTML_MARK = "(<[^<]*?>)|(<[\\s]*?/[^<]*?>)|(<[^<]*?/[\\s]*?>)"; + + private static final char[][] TEXT = new char[64][]; + + static + { + for (int i = 0; i < 64; i++) + { + TEXT[i] = new char[] { (char) i }; + } + + // special HTML characters + TEXT['\''] = "'".toCharArray(); // 单引号 + TEXT['"'] = """.toCharArray(); // 双引号 + TEXT['&'] = "&".toCharArray(); // &符 + TEXT['<'] = "<".toCharArray(); // 小于号 + TEXT['>'] = ">".toCharArray(); // 大于号 + } + + /** + * 转义文本中的HTML字符为安全的字符 + * + * @param text 被转义的文本 + * @return 转义后的文本 + */ + public static String escape(String text) + { + return encode(text); + } + + /** + * 还原被转义的HTML特殊字符 + * + * @param content 包含转义符的HTML内容 + * @return 转换后的字符串 + */ + public static String unescape(String content) + { + return decode(content); + } + + /** + * 清除所有HTML标签,但是不删除标签内的内容 + * + * @param content 文本 + * @return 清除标签后的文本 + */ + public static String clean(String content) + { + return new HTMLFilter().filter(content); + } + + /** + * Escape编码 + * + * @param text 被编码的文本 + * @return 编码后的字符 + */ + private static String encode(String text) + { + if (StringUtils.isEmpty(text)) + { + return StringUtils.EMPTY; + } + + final StringBuilder tmp = new StringBuilder(text.length() * 6); + char c; + for (int i = 0; i < text.length(); i++) + { + c = text.charAt(i); + if (c < 256) + { + tmp.append("%"); + if (c < 16) + { + tmp.append("0"); + } + tmp.append(Integer.toString(c, 16)); + } + else + { + tmp.append("%u"); + if (c <= 0xfff) + { + // issue#I49JU8@Gitee + tmp.append("0"); + } + tmp.append(Integer.toString(c, 16)); + } + } + return tmp.toString(); + } + + /** + * Escape解码 + * + * @param content 被转义的内容 + * @return 解码后的字符串 + */ + public static String decode(String content) + { + if (StringUtils.isEmpty(content)) + { + return content; + } + + StringBuilder tmp = new StringBuilder(content.length()); + int lastPos = 0, pos = 0; + char ch; + while (lastPos < content.length()) + { + pos = content.indexOf("%", lastPos); + if (pos == lastPos) + { + if (content.charAt(pos + 1) == 'u') + { + ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16); + tmp.append(ch); + lastPos = pos + 6; + } + else + { + ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16); + tmp.append(ch); + lastPos = pos + 3; + } + } + else + { + if (pos == -1) + { + tmp.append(content.substring(lastPos)); + lastPos = content.length(); + } + else + { + tmp.append(content.substring(lastPos, pos)); + lastPos = pos; + } + } + } + return tmp.toString(); + } + + public static void main(String[] args) + { + String html = ""; + String escape = EscapeUtil.escape(html); + // String html = "ipt>alert(\"XSS\")ipt>"; + // String html = "<123"; + // String html = "123>"; + System.out.println("clean: " + EscapeUtil.clean(html)); + System.out.println("escape: " + escape); + System.out.println("unescape: " + EscapeUtil.unescape(escape)); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/HTMLFilter.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/HTMLFilter.java new file mode 100644 index 0000000..ebff3fd --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/html/HTMLFilter.java @@ -0,0 +1,570 @@ +package com.ruoyi.common.utils.html; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * HTML过滤器,用于去除XSS漏洞隐患。 + * + * @author ruoyi + */ +public final class HTMLFilter +{ + /** + * regex flag union representing /si modifiers in php + **/ + private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL; + private static final Pattern P_COMMENTS = Pattern.compile("", Pattern.DOTALL); + private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI); + private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL); + private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI); + private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI); + private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI); + private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI); + private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI); + private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?"); + private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?"); + private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?"); + private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))"); + private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL); + private static final Pattern P_END_ARROW = Pattern.compile("^>"); + private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_AMP = Pattern.compile("&"); + private static final Pattern P_QUOTE = Pattern.compile("\""); + private static final Pattern P_LEFT_ARROW = Pattern.compile("<"); + private static final Pattern P_RIGHT_ARROW = Pattern.compile(">"); + private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>"); + + // @xxx could grow large... maybe use sesat's ReferenceMap + private static final ConcurrentMap P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap<>(); + private static final ConcurrentMap P_REMOVE_SELF_BLANKS = new ConcurrentHashMap<>(); + + /** + * set of allowed html elements, along with allowed attributes for each element + **/ + private final Map> vAllowed; + /** + * counts of open tags for each (allowable) html element + **/ + private final Map vTagCounts = new HashMap<>(); + + /** + * html elements which must always be self-closing (e.g. "") + **/ + private final String[] vSelfClosingTags; + /** + * html elements which must always have separate opening and closing tags (e.g. "") + **/ + private final String[] vNeedClosingTags; + /** + * set of disallowed html elements + **/ + private final String[] vDisallowed; + /** + * attributes which should be checked for valid protocols + **/ + private final String[] vProtocolAtts; + /** + * allowed protocols + **/ + private final String[] vAllowedProtocols; + /** + * tags which should be removed if they contain no content (e.g. "" or "") + **/ + private final String[] vRemoveBlanks; + /** + * entities allowed within html markup + **/ + private final String[] vAllowedEntities; + /** + * flag determining whether comments are allowed in input String. + */ + private final boolean stripComment; + private final boolean encodeQuotes; + /** + * flag determining whether to try to make tags when presented with "unbalanced" angle brackets (e.g. "" + * becomes " text "). If set to false, unbalanced angle brackets will be html escaped. + */ + private final boolean alwaysMakeTags; + + /** + * Default constructor. + */ + public HTMLFilter() + { + vAllowed = new HashMap<>(); + + final ArrayList a_atts = new ArrayList<>(); + a_atts.add("href"); + a_atts.add("target"); + vAllowed.put("a", a_atts); + + final ArrayList img_atts = new ArrayList<>(); + img_atts.add("src"); + img_atts.add("width"); + img_atts.add("height"); + img_atts.add("alt"); + vAllowed.put("img", img_atts); + + final ArrayList no_atts = new ArrayList<>(); + vAllowed.put("b", no_atts); + vAllowed.put("strong", no_atts); + vAllowed.put("i", no_atts); + vAllowed.put("em", no_atts); + + vSelfClosingTags = new String[] { "img" }; + vNeedClosingTags = new String[] { "a", "b", "strong", "i", "em" }; + vDisallowed = new String[] {}; + vAllowedProtocols = new String[] { "http", "mailto", "https" }; // no ftp. + vProtocolAtts = new String[] { "src", "href" }; + vRemoveBlanks = new String[] { "a", "b", "strong", "i", "em" }; + vAllowedEntities = new String[] { "amp", "gt", "lt", "quot" }; + stripComment = true; + encodeQuotes = true; + alwaysMakeTags = false; + } + + /** + * Map-parameter configurable constructor. + * + * @param conf map containing configuration. keys match field names. + */ + @SuppressWarnings("unchecked") + public HTMLFilter(final Map conf) + { + + assert conf.containsKey("vAllowed") : "configuration requires vAllowed"; + assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags"; + assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags"; + assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed"; + assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols"; + assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts"; + assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks"; + assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities"; + + vAllowed = Collections.unmodifiableMap((HashMap>) conf.get("vAllowed")); + vSelfClosingTags = (String[]) conf.get("vSelfClosingTags"); + vNeedClosingTags = (String[]) conf.get("vNeedClosingTags"); + vDisallowed = (String[]) conf.get("vDisallowed"); + vAllowedProtocols = (String[]) conf.get("vAllowedProtocols"); + vProtocolAtts = (String[]) conf.get("vProtocolAtts"); + vRemoveBlanks = (String[]) conf.get("vRemoveBlanks"); + vAllowedEntities = (String[]) conf.get("vAllowedEntities"); + stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true; + encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true; + alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true; + } + + private void reset() + { + vTagCounts.clear(); + } + + // --------------------------------------------------------------- + // my versions of some PHP library functions + public static String chr(final int decimal) + { + return String.valueOf((char) decimal); + } + + public static String htmlSpecialChars(final String s) + { + String result = s; + result = regexReplace(P_AMP, "&", result); + result = regexReplace(P_QUOTE, """, result); + result = regexReplace(P_LEFT_ARROW, "<", result); + result = regexReplace(P_RIGHT_ARROW, ">", result); + return result; + } + + // --------------------------------------------------------------- + + /** + * given a user submitted input String, filter out any invalid or restricted html. + * + * @param input text (i.e. submitted by a user) than may contain html + * @return "clean" version of input, with only valid, whitelisted html elements allowed + */ + public String filter(final String input) + { + reset(); + String s = input; + + s = escapeComments(s); + + s = balanceHTML(s); + + s = checkTags(s); + + s = processRemoveBlanks(s); + + // s = validateEntities(s); + + return s; + } + + public boolean isAlwaysMakeTags() + { + return alwaysMakeTags; + } + + public boolean isStripComments() + { + return stripComment; + } + + private String escapeComments(final String s) + { + final Matcher m = P_COMMENTS.matcher(s); + final StringBuffer buf = new StringBuffer(); + if (m.find()) + { + final String match = m.group(1); // (.*?) + m.appendReplacement(buf, Matcher.quoteReplacement("")); + } + m.appendTail(buf); + + return buf.toString(); + } + + private String balanceHTML(String s) + { + if (alwaysMakeTags) + { + // + // try and form html + // + s = regexReplace(P_END_ARROW, "", s); + // 不追加结束标签 + s = regexReplace(P_BODY_TO_END, "<$1>", s); + s = regexReplace(P_XML_CONTENT, "$1<$2", s); + + } + else + { + // + // escape stray brackets + // + s = regexReplace(P_STRAY_LEFT_ARROW, "<$1", s); + s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2><", s); + + // + // the last regexp causes '<>' entities to appear + // (we need to do a lookahead assertion so that the last bracket can + // be used in the next pass of the regexp) + // + s = regexReplace(P_BOTH_ARROWS, "", s); + } + + return s; + } + + private String checkTags(String s) + { + Matcher m = P_TAGS.matcher(s); + + final StringBuffer buf = new StringBuffer(); + while (m.find()) + { + String replaceStr = m.group(1); + replaceStr = processTag(replaceStr); + m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr)); + } + m.appendTail(buf); + + // these get tallied in processTag + // (remember to reset before subsequent calls to filter method) + final StringBuilder sBuilder = new StringBuilder(buf.toString()); + for (String key : vTagCounts.keySet()) + { + for (int ii = 0; ii < vTagCounts.get(key); ii++) + { + sBuilder.append(""); + } + } + s = sBuilder.toString(); + + return s; + } + + private String processRemoveBlanks(final String s) + { + String result = s; + for (String tag : vRemoveBlanks) + { + if (!P_REMOVE_PAIR_BLANKS.containsKey(tag)) + { + P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?>")); + } + result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result); + if (!P_REMOVE_SELF_BLANKS.containsKey(tag)) + { + P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>")); + } + result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result); + } + + return result; + } + + private static String regexReplace(final Pattern regex_pattern, final String replacement, final String s) + { + Matcher m = regex_pattern.matcher(s); + return m.replaceAll(replacement); + } + + private String processTag(final String s) + { + // ending tags + Matcher m = P_END_TAG.matcher(s); + if (m.find()) + { + final String name = m.group(1).toLowerCase(); + if (allowed(name)) + { + if (!inArray(name, vSelfClosingTags)) + { + if (vTagCounts.containsKey(name)) + { + vTagCounts.put(name, vTagCounts.get(name) - 1); + return ""; + } + } + } + } + + // starting tags + m = P_START_TAG.matcher(s); + if (m.find()) + { + final String name = m.group(1).toLowerCase(); + final String body = m.group(2); + String ending = m.group(3); + + // debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" ); + if (allowed(name)) + { + final StringBuilder params = new StringBuilder(); + + final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body); + final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body); + final List paramNames = new ArrayList<>(); + final List paramValues = new ArrayList<>(); + while (m2.find()) + { + paramNames.add(m2.group(1)); // ([a-z0-9]+) + paramValues.add(m2.group(3)); // (.*?) + } + while (m3.find()) + { + paramNames.add(m3.group(1)); // ([a-z0-9]+) + paramValues.add(m3.group(3)); // ([^\"\\s']+) + } + + String paramName, paramValue; + for (int ii = 0; ii < paramNames.size(); ii++) + { + paramName = paramNames.get(ii).toLowerCase(); + paramValue = paramValues.get(ii); + + // debug( "paramName='" + paramName + "'" ); + // debug( "paramValue='" + paramValue + "'" ); + // debug( "allowed? " + vAllowed.get( name ).contains( paramName ) ); + + if (allowedAttribute(name, paramName)) + { + if (inArray(paramName, vProtocolAtts)) + { + paramValue = processParamProtocol(paramValue); + } + params.append(' ').append(paramName).append("=\\\"").append(paramValue).append("\\\""); + } + } + + if (inArray(name, vSelfClosingTags)) + { + ending = " /"; + } + + if (inArray(name, vNeedClosingTags)) + { + ending = ""; + } + + if (ending == null || ending.length() < 1) + { + if (vTagCounts.containsKey(name)) + { + vTagCounts.put(name, vTagCounts.get(name) + 1); + } + else + { + vTagCounts.put(name, 1); + } + } + else + { + ending = " /"; + } + return "<" + name + params + ending + ">"; + } + else + { + return ""; + } + } + + // comments + m = P_COMMENT.matcher(s); + if (!stripComment && m.find()) + { + return "<" + m.group() + ">"; + } + + return ""; + } + + private String processParamProtocol(String s) + { + s = decodeEntities(s); + final Matcher m = P_PROTOCOL.matcher(s); + if (m.find()) + { + final String protocol = m.group(1); + if (!inArray(protocol, vAllowedProtocols)) + { + // bad protocol, turn into local anchor link instead + s = "#" + s.substring(protocol.length() + 1); + if (s.startsWith("#//")) + { + s = "#" + s.substring(3); + } + } + } + + return s; + } + + private String decodeEntities(String s) + { + StringBuffer buf = new StringBuffer(); + + Matcher m = P_ENTITY.matcher(s); + while (m.find()) + { + final String match = m.group(1); + final int decimal = Integer.decode(match).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENTITY_UNICODE.matcher(s); + while (m.find()) + { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENCODE.matcher(s); + while (m.find()) + { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + s = validateEntities(s); + return s; + } + + private String validateEntities(final String s) + { + StringBuffer buf = new StringBuffer(); + + // validate entities throughout the string + Matcher m = P_VALID_ENTITIES.matcher(s); + while (m.find()) + { + final String one = m.group(1); // ([^&;]*) + final String two = m.group(2); // (?=(;|&|$)) + m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two))); + } + m.appendTail(buf); + + return encodeQuotes(buf.toString()); + } + + private String encodeQuotes(final String s) + { + if (encodeQuotes) + { + StringBuffer buf = new StringBuffer(); + Matcher m = P_VALID_QUOTES.matcher(s); + while (m.find()) + { + final String one = m.group(1); // (>|^) + final String two = m.group(2); // ([^<]+?) + final String three = m.group(3); // (<|$) + // 不替换双引号为",防止json格式无效 regexReplace(P_QUOTE, """, two) + m.appendReplacement(buf, Matcher.quoteReplacement(one + two + three)); + } + m.appendTail(buf); + return buf.toString(); + } + else + { + return s; + } + } + + private String checkEntity(final String preamble, final String term) + { + + return ";".equals(term) && isValidEntity(preamble) ? '&' + preamble : "&" + preamble; + } + + private boolean isValidEntity(final String entity) + { + return inArray(entity, vAllowedEntities); + } + + private static boolean inArray(final String s, final String[] array) + { + for (String item : array) + { + if (item != null && item.equals(s)) + { + return true; + } + } + return false; + } + + private boolean allowed(final String name) + { + return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed); + } + + private boolean allowedAttribute(final String name, final String paramName) + { + return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName)); + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpHelper.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpHelper.java new file mode 100644 index 0000000..589d123 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpHelper.java @@ -0,0 +1,55 @@ +package com.ruoyi.common.utils.http; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import javax.servlet.ServletRequest; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 通用http工具封装 + * + * @author ruoyi + */ +public class HttpHelper +{ + private static final Logger LOGGER = LoggerFactory.getLogger(HttpHelper.class); + + public static String getBodyString(ServletRequest request) + { + StringBuilder sb = new StringBuilder(); + BufferedReader reader = null; + try (InputStream inputStream = request.getInputStream()) + { + reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + String line = ""; + while ((line = reader.readLine()) != null) + { + sb.append(line); + } + } + catch (IOException e) + { + LOGGER.warn("getBodyString出现问题!"); + } + finally + { + if (reader != null) + { + try + { + reader.close(); + } + catch (IOException e) + { + LOGGER.error(ExceptionUtils.getMessage(e)); + } + } + } + return sb.toString(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java new file mode 100644 index 0000000..534d21c --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java @@ -0,0 +1,293 @@ +package com.ruoyi.common.utils.http; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.utils.StringUtils; +import org.springframework.http.MediaType; + +/** + * 通用http发送方法 + * + * @author ruoyi + */ +public class HttpUtils +{ + private static final Logger log = LoggerFactory.getLogger(HttpUtils.class); + + /** + * 向指定 URL 发送GET方法的请求 + * + * @param url 发送请求的 URL + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url) + { + return sendGet(url, StringUtils.EMPTY); + } + + /** + * 向指定 URL 发送GET方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url, String param) + { + return sendGet(url, param, Constants.UTF8); + } + + /** + * 向指定 URL 发送GET方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @param contentType 编码类型 + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url, String param, String contentType) + { + StringBuilder result = new StringBuilder(); + BufferedReader in = null; + try + { + String urlNameString = StringUtils.isNotBlank(param) ? url + "?" + param : url; + log.info("sendGet - {}", urlNameString); + URL realUrl = new URL(urlNameString); + URLConnection connection = realUrl.openConnection(); + connection.setRequestProperty("accept", "*/*"); + connection.setRequestProperty("connection", "Keep-Alive"); + connection.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); + connection.connect(); + in = new BufferedReader(new InputStreamReader(connection.getInputStream(), contentType)); + String line; + while ((line = in.readLine()) != null) + { + result.append(line); + } + log.info("recv - {}", result); + } + catch (ConnectException e) + { + log.error("调用HttpUtils.sendGet ConnectException, url=" + url + ",param=" + param, e); + } + catch (SocketTimeoutException e) + { + log.error("调用HttpUtils.sendGet SocketTimeoutException, url=" + url + ",param=" + param, e); + } + catch (IOException e) + { + log.error("调用HttpUtils.sendGet IOException, url=" + url + ",param=" + param, e); + } + catch (Exception e) + { + log.error("调用HttpsUtil.sendGet Exception, url=" + url + ",param=" + param, e); + } + finally + { + try + { + if (in != null) + { + in.close(); + } + } + catch (Exception ex) + { + log.error("调用in.close Exception, url=" + url + ",param=" + param, ex); + } + } + return result.toString(); + } + + /** + * 向指定 URL 发送POST方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @return 所代表远程资源的响应结果 + */ + public static String sendPost(String url, String param) + { + return sendPost(url, param, MediaType.APPLICATION_FORM_URLENCODED_VALUE); + } + + /** + * 向指定 URL 发送POST方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数 + * @param contentType 内容类型 + * @return 所代表远程资源的响应结果 + */ + public static String sendPost(String url, String param, String contentType) + { + PrintWriter out = null; + BufferedReader in = null; + StringBuilder result = new StringBuilder(); + try + { + log.info("sendPost - {}", url); + URL realUrl = new URL(url); + URLConnection conn = realUrl.openConnection(); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("connection", "Keep-Alive"); + conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); + conn.setRequestProperty("Accept-Charset", "utf-8"); + conn.setRequestProperty("Content-Type", contentType); + conn.setDoOutput(true); + conn.setDoInput(true); + out = new PrintWriter(conn.getOutputStream()); + out.print(param); + out.flush(); + in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); + String line; + while ((line = in.readLine()) != null) + { + result.append(line); + } + log.info("recv - {}", result); + } + catch (ConnectException e) + { + log.error("调用HttpUtils.sendPost ConnectException, url=" + url + ",param=" + param, e); + } + catch (SocketTimeoutException e) + { + log.error("调用HttpUtils.sendPost SocketTimeoutException, url=" + url + ",param=" + param, e); + } + catch (IOException e) + { + log.error("调用HttpUtils.sendPost IOException, url=" + url + ",param=" + param, e); + } + catch (Exception e) + { + log.error("调用HttpsUtil.sendPost Exception, url=" + url + ",param=" + param, e); + } + finally + { + try + { + if (out != null) + { + out.close(); + } + if (in != null) + { + in.close(); + } + } + catch (IOException ex) + { + log.error("调用in.close Exception, url=" + url + ",param=" + param, ex); + } + } + return result.toString(); + } + + public static String sendSSLPost(String url, String param) + { + return sendSSLPost(url, param, MediaType.APPLICATION_FORM_URLENCODED_VALUE); + } + + public static String sendSSLPost(String url, String param, String contentType) + { + StringBuilder result = new StringBuilder(); + String urlNameString = url + "?" + param; + try + { + log.info("sendSSLPost - {}", urlNameString); + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, new TrustManager[] { new TrustAnyTrustManager() }, new java.security.SecureRandom()); + URL console = new URL(urlNameString); + HttpsURLConnection conn = (HttpsURLConnection) console.openConnection(); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("connection", "Keep-Alive"); + conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); + conn.setRequestProperty("Accept-Charset", "utf-8"); + conn.setRequestProperty("Content-Type", contentType); + conn.setDoOutput(true); + conn.setDoInput(true); + + conn.setSSLSocketFactory(sc.getSocketFactory()); + conn.setHostnameVerifier(new TrustAnyHostnameVerifier()); + conn.connect(); + InputStream is = conn.getInputStream(); + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + String ret = ""; + while ((ret = br.readLine()) != null) + { + if (ret != null && !"".equals(ret.trim())) + { + result.append(new String(ret.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8)); + } + } + log.info("recv - {}", result); + conn.disconnect(); + br.close(); + } + catch (ConnectException e) + { + log.error("调用HttpUtils.sendSSLPost ConnectException, url=" + url + ",param=" + param, e); + } + catch (SocketTimeoutException e) + { + log.error("调用HttpUtils.sendSSLPost SocketTimeoutException, url=" + url + ",param=" + param, e); + } + catch (IOException e) + { + log.error("调用HttpUtils.sendSSLPost IOException, url=" + url + ",param=" + param, e); + } + catch (Exception e) + { + log.error("调用HttpsUtil.sendSSLPost Exception, url=" + url + ",param=" + param, e); + } + return result.toString(); + } + + private static class TrustAnyTrustManager implements X509TrustManager + { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) + { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + { + } + + @Override + public X509Certificate[] getAcceptedIssuers() + { + return new X509Certificate[] {}; + } + } + + private static class TrustAnyHostnameVerifier implements HostnameVerifier + { + @Override + public boolean verify(String hostname, SSLSession session) + { + return true; + } + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/AddressUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/AddressUtils.java new file mode 100644 index 0000000..edfe419 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/AddressUtils.java @@ -0,0 +1,56 @@ +package com.ruoyi.common.utils.ip; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.http.HttpUtils; + +/** + * 获取地址类 + * + * @author ruoyi + */ +public class AddressUtils +{ + private static final Logger log = LoggerFactory.getLogger(AddressUtils.class); + + // IP地址查询 + public static final String IP_URL = "http://whois.pconline.com.cn/ipJson.jsp"; + + // 未知地址 + public static final String UNKNOWN = "XX XX"; + + public static String getRealAddressByIP(String ip) + { + // 内网不查询 + if (IpUtils.internalIp(ip)) + { + return "内网IP"; + } + if (RuoYiConfig.isAddressEnabled()) + { + try + { + String rspStr = HttpUtils.sendGet(IP_URL, "ip=" + ip + "&json=true", Constants.GBK); + if (StringUtils.isEmpty(rspStr)) + { + log.error("获取地理位置异常 {}", ip); + return UNKNOWN; + } + JSONObject obj = JSON.parseObject(rspStr); + String region = obj.getString("pro"); + String city = obj.getString("city"); + return String.format("%s %s", region, city); + } + catch (Exception e) + { + log.error("获取地理位置异常 {}", ip); + } + } + return UNKNOWN; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/IpUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/IpUtils.java new file mode 100644 index 0000000..8e89e30 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/ip/IpUtils.java @@ -0,0 +1,382 @@ +package com.ruoyi.common.utils.ip; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import javax.servlet.http.HttpServletRequest; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; + +/** + * 获取IP方法 + * + * @author ruoyi + */ +public class IpUtils +{ + public final static String REGX_0_255 = "(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]\\d|\\d)"; + // 匹配 ip + public final static String REGX_IP = "((" + REGX_0_255 + "\\.){3}" + REGX_0_255 + ")"; + public final static String REGX_IP_WILDCARD = "(((\\*\\.){3}\\*)|(" + REGX_0_255 + "(\\.\\*){3})|(" + REGX_0_255 + "\\." + REGX_0_255 + ")(\\.\\*){2}" + "|((" + REGX_0_255 + "\\.){3}\\*))"; + // 匹配网段 + public final static String REGX_IP_SEG = "(" + REGX_IP + "\\-" + REGX_IP + ")"; + + /** + * 获取客户端IP + * + * @return IP地址 + */ + public static String getIpAddr() + { + return getIpAddr(ServletUtils.getRequest()); + } + + /** + * 获取客户端IP + * + * @param request 请求对象 + * @return IP地址 + */ + public static String getIpAddr(HttpServletRequest request) + { + if (request == null) + { + return "unknown"; + } + 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("X-Forwarded-For"); + } + 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.getHeader("X-Real-IP"); + } + + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getRemoteAddr(); + } + + return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip); + } + + /** + * 检查是否为内部IP地址 + * + * @param ip IP地址 + * @return 结果 + */ + public static boolean internalIp(String ip) + { + byte[] addr = textToNumericFormatV4(ip); + return internalIp(addr) || "127.0.0.1".equals(ip); + } + + /** + * 检查是否为内部IP地址 + * + * @param addr byte地址 + * @return 结果 + */ + private static boolean internalIp(byte[] addr) + { + if (StringUtils.isNull(addr) || addr.length < 2) + { + return true; + } + final byte b0 = addr[0]; + final byte b1 = addr[1]; + // 10.x.x.x/8 + final byte SECTION_1 = 0x0A; + // 172.16.x.x/12 + final byte SECTION_2 = (byte) 0xAC; + final byte SECTION_3 = (byte) 0x10; + final byte SECTION_4 = (byte) 0x1F; + // 192.168.x.x/16 + final byte SECTION_5 = (byte) 0xC0; + final byte SECTION_6 = (byte) 0xA8; + switch (b0) + { + case SECTION_1: + return true; + case SECTION_2: + if (b1 >= SECTION_3 && b1 <= SECTION_4) + { + return true; + } + case SECTION_5: + switch (b1) + { + case SECTION_6: + return true; + } + default: + return false; + } + } + + /** + * 将IPv4地址转换成字节 + * + * @param text IPv4地址 + * @return byte 字节 + */ + public static byte[] textToNumericFormatV4(String text) + { + if (text.length() == 0) + { + return null; + } + + byte[] bytes = new byte[4]; + String[] elements = text.split("\\.", -1); + try + { + long l; + int i; + switch (elements.length) + { + case 1: + l = Long.parseLong(elements[0]); + if ((l < 0L) || (l > 4294967295L)) + { + return null; + } + bytes[0] = (byte) (int) (l >> 24 & 0xFF); + bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 2: + l = Integer.parseInt(elements[0]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[0] = (byte) (int) (l & 0xFF); + l = Integer.parseInt(elements[1]); + if ((l < 0L) || (l > 16777215L)) + { + return null; + } + bytes[1] = (byte) (int) (l >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 3: + for (i = 0; i < 2; ++i) + { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + l = Integer.parseInt(elements[2]); + if ((l < 0L) || (l > 65535L)) + { + return null; + } + bytes[2] = (byte) (int) (l >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 4: + for (i = 0; i < 4; ++i) + { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + break; + default: + return null; + } + } + catch (NumberFormatException e) + { + return null; + } + return bytes; + } + + /** + * 获取IP地址 + * + * @return 本地IP地址 + */ + public static String getHostIp() + { + try + { + return InetAddress.getLocalHost().getHostAddress(); + } + catch (UnknownHostException e) + { + } + return "127.0.0.1"; + } + + /** + * 获取主机名 + * + * @return 本地主机名 + */ + public static String getHostName() + { + try + { + return InetAddress.getLocalHost().getHostName(); + } + catch (UnknownHostException e) + { + } + return "未知"; + } + + /** + * 从多级反向代理中获得第一个非unknown IP地址 + * + * @param ip 获得的IP地址 + * @return 第一个非unknown IP地址 + */ + public static String getMultistageReverseProxyIp(String ip) + { + // 多级反向代理检测 + if (ip != null && ip.indexOf(",") > 0) + { + final String[] ips = ip.trim().split(","); + for (String subIp : ips) + { + if (false == isUnknown(subIp)) + { + ip = subIp; + break; + } + } + } + return StringUtils.substring(ip, 0, 255); + } + + /** + * 检测给定字符串是否为未知,多用于检测HTTP请求相关 + * + * @param checkString 被检测的字符串 + * @return 是否未知 + */ + public static boolean isUnknown(String checkString) + { + return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString); + } + + /** + * 是否为IP + */ + public static boolean isIP(String ip) + { + return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP); + } + + /** + * 是否为IP,或 *为间隔的通配符地址 + */ + public static boolean isIpWildCard(String ip) + { + return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP_WILDCARD); + } + + /** + * 检测参数是否在ip通配符里 + */ + public static boolean ipIsInWildCardNoCheck(String ipWildCard, String ip) + { + String[] s1 = ipWildCard.split("\\."); + String[] s2 = ip.split("\\."); + boolean isMatchedSeg = true; + for (int i = 0; i < s1.length && !s1[i].equals("*"); i++) + { + if (!s1[i].equals(s2[i])) + { + isMatchedSeg = false; + break; + } + } + return isMatchedSeg; + } + + /** + * 是否为特定格式如:“10.10.10.1-10.10.10.99”的ip段字符串 + */ + public static boolean isIPSegment(String ipSeg) + { + return StringUtils.isNotBlank(ipSeg) && ipSeg.matches(REGX_IP_SEG); + } + + /** + * 判断ip是否在指定网段中 + */ + public static boolean ipIsInNetNoCheck(String iparea, String ip) + { + int idx = iparea.indexOf('-'); + String[] sips = iparea.substring(0, idx).split("\\."); + String[] sipe = iparea.substring(idx + 1).split("\\."); + String[] sipt = ip.split("\\."); + long ips = 0L, ipe = 0L, ipt = 0L; + for (int i = 0; i < 4; ++i) + { + ips = ips << 8 | Integer.parseInt(sips[i]); + ipe = ipe << 8 | Integer.parseInt(sipe[i]); + ipt = ipt << 8 | Integer.parseInt(sipt[i]); + } + if (ips > ipe) + { + long t = ips; + ips = ipe; + ipe = t; + } + return ips <= ipt && ipt <= ipe; + } + + /** + * 校验ip是否符合过滤串规则 + * + * @param filter 过滤IP列表,支持后缀'*'通配,支持网段如:`10.10.10.1-10.10.10.99` + * @param ip 校验IP地址 + * @return boolean 结果 + */ + public static boolean isMatchedIp(String filter, String ip) + { + if (StringUtils.isEmpty(filter) || StringUtils.isEmpty(ip)) + { + return false; + } + String[] ips = filter.split(";"); + for (String iStr : ips) + { + if (isIP(iStr) && iStr.equals(ip)) + { + return true; + } + else if (isIpWildCard(iStr) && ipIsInWildCardNoCheck(iStr, ip)) + { + return true; + } + else if (isIPSegment(iStr) && ipIsInNetNoCheck(iStr, ip)) + { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelHandlerAdapter.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelHandlerAdapter.java new file mode 100644 index 0000000..ccab288 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelHandlerAdapter.java @@ -0,0 +1,24 @@ +package com.ruoyi.common.utils.poi; + +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Workbook; + +/** + * Excel数据格式处理适配器 + * + * @author ruoyi + */ +public interface ExcelHandlerAdapter +{ + /** + * 格式化 + * + * @param value 单元格数据值 + * @param args excel注解args参数组 + * @param cell 单元格对象 + * @param wb 工作簿对象 + * + * @return 处理后的值 + */ + Object format(Object value, String[] args, Cell cell, Workbook wb); +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java new file mode 100644 index 0000000..c4191ef --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java @@ -0,0 +1,1893 @@ +package com.ruoyi.common.utils.poi; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.RegExUtils; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.poi.hssf.usermodel.HSSFClientAnchor; +import org.apache.poi.hssf.usermodel.HSSFPicture; +import org.apache.poi.hssf.usermodel.HSSFPictureData; +import org.apache.poi.hssf.usermodel.HSSFShape; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ooxml.POIXMLDocumentPart; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.ClientAnchor; +import org.apache.poi.ss.usermodel.DataFormat; +import org.apache.poi.ss.usermodel.DataValidation; +import org.apache.poi.ss.usermodel.DataValidationConstraint; +import org.apache.poi.ss.usermodel.DataValidationHelper; +import org.apache.poi.ss.usermodel.DateUtil; +import org.apache.poi.ss.usermodel.Drawing; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.Name; +import org.apache.poi.ss.usermodel.PictureData; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.ss.util.CellRangeAddressList; +import org.apache.poi.util.IOUtils; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; +import org.apache.poi.xssf.usermodel.XSSFClientAnchor; +import org.apache.poi.xssf.usermodel.XSSFDataValidation; +import org.apache.poi.xssf.usermodel.XSSFDrawing; +import org.apache.poi.xssf.usermodel.XSSFPicture; +import org.apache.poi.xssf.usermodel.XSSFShape; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTMarker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.annotation.Excel.Type; +import com.ruoyi.common.annotation.Excels; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.exception.UtilException; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.DictUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.file.FileTypeUtils; +import com.ruoyi.common.utils.file.FileUtils; +import com.ruoyi.common.utils.file.ImageUtils; +import com.ruoyi.common.utils.reflect.ReflectUtils; + +/** + * Excel相关处理 + * + * @author ruoyi + */ +public class ExcelUtil +{ + private static final Logger log = LoggerFactory.getLogger(ExcelUtil.class); + + public static final String SEPARATOR = ","; + + public static final String FORMULA_REGEX_STR = "=|-|\\+|@"; + + public static final String[] FORMULA_STR = { "=", "-", "+", "@" }; + + /** + * 用于dictType属性数据存储,避免重复查缓存 + */ + public Map sysDictMap = new HashMap(); + + /** + * Excel sheet最大行数,默认65536 + */ + public static final int sheetSize = 65536; + + /** + * 工作表名称 + */ + private String sheetName; + + /** + * 导出类型(EXPORT:导出数据;IMPORT:导入模板) + */ + private Type type; + + /** + * 工作薄对象 + */ + private Workbook wb; + + /** + * 工作表对象 + */ + private Sheet sheet; + + /** + * 样式列表 + */ + private Map styles; + + /** + * 导入导出数据列表 + */ + private List list; + + /** + * 注解列表 + */ + private List fields; + + /** + * 当前行号 + */ + private int rownum; + + /** + * 标题 + */ + private String title; + + /** + * 最大高度 + */ + private short maxHeight; + + /** + * 合并后最后行数 + */ + private int subMergedLastRowNum = 0; + + /** + * 合并后开始行数 + */ + private int subMergedFirstRowNum = 1; + + /** + * 对象的子列表方法 + */ + private Method subMethod; + + /** + * 对象的子列表属性 + */ + private List subFields; + + /** + * 统计列表 + */ + private Map statistics = new HashMap(); + + /** + * 实体对象 + */ + public Class clazz; + + /** + * 需要显示列属性 + */ + public String[] includeFields; + + /** + * 需要排除列属性 + */ + public String[] excludeFields; + + public ExcelUtil(Class clazz) + { + this.clazz = clazz; + } + + /** + * 仅在Excel中显示列属性 + * + * @param fields 列属性名 示例[单个"name"/多个"id","name"] + */ + public void showColumn(String... fields) + { + this.includeFields = fields; + } + + /** + * 隐藏Excel中列属性 + * + * @param fields 列属性名 示例[单个"name"/多个"id","name"] + */ + public void hideColumn(String... fields) + { + this.excludeFields = fields; + } + + public void init(List list, String sheetName, String title, Type type) + { + if (list == null) + { + list = new ArrayList(); + } + this.list = list; + this.sheetName = sheetName; + this.type = type; + this.title = title; + createExcelField(); + createWorkbook(); + createTitle(); + createSubHead(); + } + + /** + * 创建excel第一行标题 + */ + public void createTitle() + { + if (StringUtils.isNotEmpty(title)) + { + int titleLastCol = this.fields.size() - 1; + if (isSubList()) + { + titleLastCol = titleLastCol + subFields.size() - 1; + } + Row titleRow = sheet.createRow(rownum == 0 ? rownum++ : 0); + titleRow.setHeightInPoints(30); + Cell titleCell = titleRow.createCell(0); + titleCell.setCellStyle(styles.get("title")); + titleCell.setCellValue(title); + sheet.addMergedRegion(new CellRangeAddress(titleRow.getRowNum(), titleRow.getRowNum(), 0, titleLastCol)); + } + } + + /** + * 创建对象的子列表名称 + */ + public void createSubHead() + { + if (isSubList()) + { + Row subRow = sheet.createRow(rownum); + int column = 0; + int subFieldSize = subFields != null ? subFields.size() : 0; + for (Object[] objects : fields) + { + Field field = (Field) objects[0]; + Excel attr = (Excel) objects[1]; + if (Collection.class.isAssignableFrom(field.getType())) + { + Cell cell = subRow.createCell(column); + cell.setCellValue(attr.name()); + cell.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor()))); + if (subFieldSize > 1) + { + CellRangeAddress cellAddress = new CellRangeAddress(rownum, rownum, column, column + subFieldSize - 1); + sheet.addMergedRegion(cellAddress); + } + column += subFieldSize; + } + else + { + Cell cell = subRow.createCell(column++); + cell.setCellValue(attr.name()); + cell.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor()))); + } + } + rownum++; + } + } + + /** + * 对excel表单默认第一个索引名转换成list + * + * @param is 输入流 + * @return 转换后集合 + */ + public List importExcel(InputStream is) + { + return importExcel(is, 0); + } + + /** + * 对excel表单默认第一个索引名转换成list + * + * @param is 输入流 + * @param titleNum 标题占用行数 + * @return 转换后集合 + */ + public List importExcel(InputStream is, int titleNum) + { + List list = null; + try + { + list = importExcel(StringUtils.EMPTY, is, titleNum); + } + catch (Exception e) + { + log.error("导入Excel异常{}", e.getMessage()); + throw new UtilException(e.getMessage()); + } + finally + { + IOUtils.closeQuietly(is); + } + return list; + } + + /** + * 对excel表单指定表格索引名转换成list + * + * @param sheetName 表格索引名 + * @param titleNum 标题占用行数 + * @param is 输入流 + * @return 转换后集合 + */ + public List importExcel(String sheetName, InputStream is, int titleNum) throws Exception + { + this.type = Type.IMPORT; + this.wb = WorkbookFactory.create(is); + List list = new ArrayList(); + // 如果指定sheet名,则取指定sheet中的内容 否则默认指向第1个sheet + Sheet sheet = StringUtils.isNotEmpty(sheetName) ? wb.getSheet(sheetName) : wb.getSheetAt(0); + if (sheet == null) + { + throw new IOException("文件sheet不存在"); + } + boolean isXSSFWorkbook = !(wb instanceof HSSFWorkbook); + Map> pictures = null; + if (isXSSFWorkbook) + { + pictures = getSheetPictures07((XSSFSheet) sheet, (XSSFWorkbook) wb); + } + else + { + pictures = getSheetPictures03((HSSFSheet) sheet, (HSSFWorkbook) wb); + } + // 获取最后一个非空行的行下标,比如总行数为n,则返回的为n-1 + int rows = sheet.getLastRowNum(); + if (rows > 0) + { + // 定义一个map用于存放excel列的序号和field. + Map cellMap = new HashMap(); + // 获取表头 + Row heard = sheet.getRow(titleNum); + for (int i = 0; i < heard.getPhysicalNumberOfCells(); i++) + { + Cell cell = heard.getCell(i); + if (StringUtils.isNotNull(cell)) + { + String value = this.getCellValue(heard, i).toString(); + cellMap.put(value, i); + } + else + { + cellMap.put(null, i); + } + } + // 有数据时才处理 得到类的所有field. + List fields = this.getFields(); + Map fieldsMap = new HashMap(); + for (Object[] objects : fields) + { + Excel attr = (Excel) objects[1]; + Integer column = cellMap.get(attr.name()); + if (column != null) + { + fieldsMap.put(column, objects); + } + } + for (int i = titleNum + 1; i <= rows; i++) + { + // 从第2行开始取数据,默认第一行是表头. + Row row = sheet.getRow(i); + // 判断当前行是否是空行 + if (isRowEmpty(row)) + { + continue; + } + T entity = null; + for (Map.Entry entry : fieldsMap.entrySet()) + { + Object val = this.getCellValue(row, entry.getKey()); + + // 如果不存在实例则新建. + entity = (entity == null ? clazz.newInstance() : entity); + // 从map中得到对应列的field. + Field field = (Field) entry.getValue()[0]; + Excel attr = (Excel) entry.getValue()[1]; + // 取得类型,并根据对象类型设置值. + Class fieldType = field.getType(); + if (String.class == fieldType) + { + String s = Convert.toStr(val); + if (s.matches("^\\d+\\.0$")) + { + val = StringUtils.substringBefore(s, ".0"); + } + else + { + String dateFormat = field.getAnnotation(Excel.class).dateFormat(); + if (StringUtils.isNotEmpty(dateFormat)) + { + val = parseDateToStr(dateFormat, val); + } + else + { + val = Convert.toStr(val); + } + } + } + else if ((Integer.TYPE == fieldType || Integer.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val))) + { + val = Convert.toInt(val); + } + else if ((Long.TYPE == fieldType || Long.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val))) + { + val = Convert.toLong(val); + } + else if (Double.TYPE == fieldType || Double.class == fieldType) + { + val = Convert.toDouble(val); + } + else if (Float.TYPE == fieldType || Float.class == fieldType) + { + val = Convert.toFloat(val); + } + else if (BigDecimal.class == fieldType) + { + val = Convert.toBigDecimal(val); + } + else if (Date.class == fieldType) + { + if (val instanceof String) + { + val = DateUtils.parseDate(val); + } + else if (val instanceof Double) + { + val = DateUtil.getJavaDate((Double) val); + } + } + else if (Boolean.TYPE == fieldType || Boolean.class == fieldType) + { + val = Convert.toBool(val, false); + } + if (StringUtils.isNotNull(fieldType)) + { + String propertyName = field.getName(); + if (StringUtils.isNotEmpty(attr.targetAttr())) + { + propertyName = field.getName() + "." + attr.targetAttr(); + } + if (StringUtils.isNotEmpty(attr.readConverterExp())) + { + val = reverseByExp(Convert.toStr(val), attr.readConverterExp(), attr.separator()); + } + else if (StringUtils.isNotEmpty(attr.dictType())) + { + if (!sysDictMap.containsKey(attr.dictType() + val)) + { + String dictValue = reverseDictByExp(Convert.toStr(val), attr.dictType(), attr.separator()); + sysDictMap.put(attr.dictType() + val, dictValue); + } + val = sysDictMap.get(attr.dictType() + val); + } + else if (!attr.handler().equals(ExcelHandlerAdapter.class)) + { + val = dataFormatHandlerAdapter(val, attr, null); + } + else if (ColumnType.IMAGE == attr.cellType() && StringUtils.isNotEmpty(pictures)) + { + StringBuilder propertyString = new StringBuilder(); + List images = pictures.get(row.getRowNum() + "_" + entry.getKey()); + for (PictureData picture : images) + { + byte[] data = picture.getData(); + String fileName = FileUtils.writeImportBytes(data); + propertyString.append(fileName).append(SEPARATOR); + } + val = StringUtils.stripEnd(propertyString.toString(), SEPARATOR); + } + ReflectUtils.invokeSetter(entity, propertyName, val); + } + } + list.add(entity); + } + } + return list; + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @return 结果 + */ + public AjaxResult exportExcel(List list, String sheetName) + { + return exportExcel(list, sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public AjaxResult exportExcel(List list, String sheetName, String title) + { + this.init(list, sheetName, title, Type.EXPORT); + return exportExcel(); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param response 返回数据 + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @return 结果 + */ + public void exportExcel(HttpServletResponse response, List list, String sheetName) + { + exportExcel(response, list, sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param response 返回数据 + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public void exportExcel(HttpServletResponse response, List list, String sheetName, String title) + { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + this.init(list, sheetName, title, Type.EXPORT); + exportExcel(response); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @return 结果 + */ + public AjaxResult importTemplateExcel(String sheetName) + { + return importTemplateExcel(sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public AjaxResult importTemplateExcel(String sheetName, String title) + { + this.init(null, sheetName, title, Type.IMPORT); + return exportExcel(); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @return 结果 + */ + public void importTemplateExcel(HttpServletResponse response, String sheetName) + { + importTemplateExcel(response, sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public void importTemplateExcel(HttpServletResponse response, String sheetName, String title) + { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + this.init(null, sheetName, title, Type.IMPORT); + exportExcel(response); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @return 结果 + */ + public void exportExcel(HttpServletResponse response) + { + try + { + writeSheet(); + wb.write(response.getOutputStream()); + } + catch (Exception e) + { + log.error("导出Excel异常{}", e.getMessage()); + } + finally + { + IOUtils.closeQuietly(wb); + } + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @return 结果 + */ + public AjaxResult exportExcel() + { + OutputStream out = null; + try + { + writeSheet(); + String filename = encodingFilename(sheetName); + out = new FileOutputStream(getAbsoluteFile(filename)); + wb.write(out); + return AjaxResult.success(filename); + } + catch (Exception e) + { + log.error("导出Excel异常{}", e.getMessage()); + throw new UtilException("导出Excel失败,请联系网站管理员!"); + } + finally + { + IOUtils.closeQuietly(wb); + IOUtils.closeQuietly(out); + } + } + + /** + * 创建写入数据到Sheet + */ + public void writeSheet() + { + // 取出一共有多少个sheet. + int sheetNo = Math.max(1, (int) Math.ceil(list.size() * 1.0 / sheetSize)); + for (int index = 0; index < sheetNo; index++) + { + createSheet(sheetNo, index); + + // 产生一行 + Row row = sheet.createRow(rownum); + int column = 0; + // 写入各个字段的列头名称 + for (Object[] os : fields) + { + Field field = (Field) os[0]; + Excel excel = (Excel) os[1]; + if (Collection.class.isAssignableFrom(field.getType())) + { + for (Field subField : subFields) + { + Excel subExcel = subField.getAnnotation(Excel.class); + this.createHeadCell(subExcel, row, column++); + } + } + else + { + this.createHeadCell(excel, row, column++); + } + } + if (Type.EXPORT.equals(type)) + { + fillExcelData(index, row); + addStatisticsRow(); + } + } + } + + /** + * 填充excel数据 + * + * @param index 序号 + * @param row 单元格行 + */ + @SuppressWarnings("unchecked") + public void fillExcelData(int index, Row row) + { + int startNo = index * sheetSize; + int endNo = Math.min(startNo + sheetSize, list.size()); + int currentRowNum = rownum + 1; // 从标题行后开始 + + for (int i = startNo; i < endNo; i++) + { + row = sheet.createRow(currentRowNum); + T vo = (T) list.get(i); + int column = 0; + int maxSubListSize = getCurrentMaxSubListSize(vo); + for (Object[] os : fields) + { + Field field = (Field) os[0]; + Excel excel = (Excel) os[1]; + if (Collection.class.isAssignableFrom(field.getType())) + { + try + { + Collection subList = (Collection) getTargetValue(vo, field, excel); + if (subList != null && !subList.isEmpty()) + { + int subIndex = 0; + for (Object subVo : subList) + { + Row subRow = sheet.getRow(currentRowNum + subIndex); + if (subRow == null) + { + subRow = sheet.createRow(currentRowNum + subIndex); + } + + int subColumn = column; + for (Field subField : subFields) + { + Excel subExcel = subField.getAnnotation(Excel.class); + addCell(subExcel, subRow, (T) subVo, subField, subColumn++); + } + subIndex++; + } + column += subFields.size(); + } + } + catch (Exception e) + { + log.error("填充集合数据失败", e); + } + } + else + { + // 创建单元格并设置值 + addCell(excel, row, vo, field, column); + if (maxSubListSize > 1 && excel.needMerge()) + { + sheet.addMergedRegion(new CellRangeAddress(currentRowNum, currentRowNum + maxSubListSize - 1, column, column)); + } + column++; + } + } + currentRowNum += maxSubListSize; + } + } + + /** + * 获取子列表最大数 + */ + private int getCurrentMaxSubListSize(T vo) + { + int maxSubListSize = 1; + for (Object[] os : fields) + { + Field field = (Field) os[0]; + if (Collection.class.isAssignableFrom(field.getType())) + { + try + { + Collection subList = (Collection) getTargetValue(vo, field, (Excel) os[1]); + if (subList != null && !subList.isEmpty()) + { + maxSubListSize = Math.max(maxSubListSize, subList.size()); + } + } + catch (Exception e) + { + log.error("获取集合大小失败", e); + } + } + } + return maxSubListSize; + } + + /** + * 创建表格样式 + * + * @param wb 工作薄对象 + * @return 样式列表 + */ + private Map createStyles(Workbook wb) + { + // 写入各条记录,每条记录对应excel表中的一行 + Map styles = new HashMap(); + CellStyle style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + Font titleFont = wb.createFont(); + titleFont.setFontName("Arial"); + titleFont.setFontHeightInPoints((short) 16); + titleFont.setBold(true); + style.setFont(titleFont); + DataFormat dataFormat = wb.createDataFormat(); + style.setDataFormat(dataFormat.getFormat("@")); + styles.put("title", style); + + style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setBorderRight(BorderStyle.THIN); + style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderLeft(BorderStyle.THIN); + style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderTop(BorderStyle.THIN); + style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderBottom(BorderStyle.THIN); + style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + Font dataFont = wb.createFont(); + dataFont.setFontName("Arial"); + dataFont.setFontHeightInPoints((short) 10); + style.setFont(dataFont); + styles.put("data", style); + + style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setDataFormat(dataFormat.getFormat("######0.00")); + Font totalFont = wb.createFont(); + totalFont.setFontName("Arial"); + totalFont.setFontHeightInPoints((short) 10); + style.setFont(totalFont); + styles.put("total", style); + + styles.putAll(annotationHeaderStyles(wb, styles)); + + styles.putAll(annotationDataStyles(wb)); + + return styles; + } + + /** + * 根据Excel注解创建表格头样式 + * + * @param wb 工作薄对象 + * @return 自定义样式列表 + */ + private Map annotationHeaderStyles(Workbook wb, Map styles) + { + Map headerStyles = new HashMap(); + for (Object[] os : fields) + { + Excel excel = (Excel) os[1]; + String key = StringUtils.format("header_{}_{}", excel.headerColor(), excel.headerBackgroundColor()); + if (!headerStyles.containsKey(key)) + { + CellStyle style = wb.createCellStyle(); + style.cloneStyleFrom(styles.get("data")); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setFillForegroundColor(excel.headerBackgroundColor().index); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + Font headerFont = wb.createFont(); + headerFont.setFontName("Arial"); + headerFont.setFontHeightInPoints((short) 10); + headerFont.setBold(true); + headerFont.setColor(excel.headerColor().index); + style.setFont(headerFont); + // 设置表格头单元格文本形式 + DataFormat dataFormat = wb.createDataFormat(); + style.setDataFormat(dataFormat.getFormat("@")); + headerStyles.put(key, style); + } + } + return headerStyles; + } + + /** + * 根据Excel注解创建表格列样式 + * + * @param wb 工作薄对象 + * @return 自定义样式列表 + */ + private Map annotationDataStyles(Workbook wb) + { + Map styles = new HashMap(); + for (Object[] os : fields) + { + Field field = (Field) os[0]; + Excel excel = (Excel) os[1]; + if (Collection.class.isAssignableFrom(field.getType())) + { + ParameterizedType pt = (ParameterizedType) field.getGenericType(); + Class subClass = (Class) pt.getActualTypeArguments()[0]; + List subFields = FieldUtils.getFieldsListWithAnnotation(subClass, Excel.class); + for (Field subField : subFields) + { + Excel subExcel = subField.getAnnotation(Excel.class); + annotationDataStyles(styles, subField, subExcel); + } + } + else + { + annotationDataStyles(styles, field, excel); + } + } + return styles; + } + + /** + * 根据Excel注解创建表格列样式 + * + * @param styles 自定义样式列表 + * @param field 属性列信息 + * @param excel 注解信息 + */ + public void annotationDataStyles(Map styles, Field field, Excel excel) + { + String key = StringUtils.format("data_{}_{}_{}_{}_{}", excel.align(), excel.color(), excel.backgroundColor(), excel.cellType(), excel.wrapText()); + if (!styles.containsKey(key)) + { + CellStyle style = wb.createCellStyle(); + style.setAlignment(excel.align()); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setBorderRight(BorderStyle.THIN); + style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderLeft(BorderStyle.THIN); + style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderTop(BorderStyle.THIN); + style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderBottom(BorderStyle.THIN); + style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + style.setFillForegroundColor(excel.backgroundColor().getIndex()); + style.setWrapText(excel.wrapText()); + Font dataFont = wb.createFont(); + dataFont.setFontName("Arial"); + dataFont.setFontHeightInPoints((short) 10); + dataFont.setColor(excel.color().index); + style.setFont(dataFont); + if (ColumnType.TEXT == excel.cellType()) + { + DataFormat dataFormat = wb.createDataFormat(); + style.setDataFormat(dataFormat.getFormat("@")); + } + styles.put(key, style); + } + } + + /** + * 创建单元格 + */ + public Cell createHeadCell(Excel attr, Row row, int column) + { + // 创建列 + Cell cell = row.createCell(column); + // 写入列信息 + cell.setCellValue(attr.name()); + setDataValidation(attr, row, column); + cell.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor()))); + if (isSubList()) + { + // 填充默认样式,防止合并单元格样式失效 + sheet.setDefaultColumnStyle(column, styles.get(StringUtils.format("data_{}_{}_{}_{}_{}", attr.align(), attr.color(), attr.backgroundColor(), attr.cellType(), attr.wrapText()))); + if (attr.needMerge()) + { + sheet.addMergedRegion(new CellRangeAddress(rownum - 1, rownum, column, column)); + } + } + return cell; + } + + /** + * 设置单元格信息 + * + * @param value 单元格值 + * @param attr 注解相关 + * @param cell 单元格信息 + */ + public void setCellVo(Object value, Excel attr, Cell cell) + { + if (ColumnType.STRING == attr.cellType() || ColumnType.TEXT == attr.cellType()) + { + String cellValue = Convert.toStr(value); + // 对于任何以表达式触发字符 =-+@开头的单元格,直接使用tab字符作为前缀,防止CSV注入。 + if (StringUtils.startsWithAny(cellValue, FORMULA_STR)) + { + cellValue = RegExUtils.replaceFirst(cellValue, FORMULA_REGEX_STR, "\t$0"); + } + if (value instanceof Collection && StringUtils.equals("[]", cellValue)) + { + cellValue = StringUtils.EMPTY; + } + cell.setCellValue(StringUtils.isNull(cellValue) ? attr.defaultValue() : cellValue + attr.suffix()); + } + else if (ColumnType.NUMERIC == attr.cellType()) + { + if (StringUtils.isNotNull(value)) + { + cell.setCellValue(StringUtils.contains(Convert.toStr(value), ".") ? Convert.toDouble(value) : Convert.toInt(value)); + } + } + else if (ColumnType.IMAGE == attr.cellType()) + { + ClientAnchor anchor = new XSSFClientAnchor(0, 0, 0, 0, (short) cell.getColumnIndex(), cell.getRow().getRowNum(), (short) (cell.getColumnIndex() + 1), cell.getRow().getRowNum() + 1); + String propertyValue = Convert.toStr(value); + if (StringUtils.isNotEmpty(propertyValue)) + { + List imagePaths = StringUtils.str2List(propertyValue, SEPARATOR); + for (String imagePath : imagePaths) + { + byte[] data = ImageUtils.getImage(imagePath); + getDrawingPatriarch(cell.getSheet()).createPicture(anchor, cell.getSheet().getWorkbook().addPicture(data, getImageType(data))); + } + } + } + } + + /** + * 获取画布 + */ + public static Drawing getDrawingPatriarch(Sheet sheet) + { + if (sheet.getDrawingPatriarch() == null) + { + sheet.createDrawingPatriarch(); + } + return sheet.getDrawingPatriarch(); + } + + /** + * 获取图片类型,设置图片插入类型 + */ + public int getImageType(byte[] value) + { + String type = FileTypeUtils.getFileExtendName(value); + if ("JPG".equalsIgnoreCase(type)) + { + return Workbook.PICTURE_TYPE_JPEG; + } + else if ("PNG".equalsIgnoreCase(type)) + { + return Workbook.PICTURE_TYPE_PNG; + } + return Workbook.PICTURE_TYPE_JPEG; + } + + /** + * 创建表格样式 + */ + public void setDataValidation(Excel attr, Row row, int column) + { + if (attr.name().indexOf("注:") >= 0) + { + sheet.setColumnWidth(column, 6000); + } + else + { + // 设置列宽 + sheet.setColumnWidth(column, (int) ((attr.width() + 0.72) * 256)); + } + if (StringUtils.isNotEmpty(attr.prompt()) || attr.combo().length > 0 || attr.comboReadDict()) + { + String[] comboArray = attr.combo(); + if (attr.comboReadDict()) + { + if (!sysDictMap.containsKey("combo_" + attr.dictType())) + { + String labels = DictUtils.getDictLabels(attr.dictType()); + sysDictMap.put("combo_" + attr.dictType(), labels); + } + String val = sysDictMap.get("combo_" + attr.dictType()); + comboArray = StringUtils.split(val, DictUtils.SEPARATOR); + } + if (comboArray.length > 15 || StringUtils.join(comboArray).length() > 255) + { + // 如果下拉数大于15或字符串长度大于255,则使用一个新sheet存储,避免生成的模板下拉值获取不到 + setXSSFValidationWithHidden(sheet, comboArray, attr.prompt(), 1, 100, column, column); + } + else + { + // 提示信息或只能选择不能输入的列内容. + setPromptOrValidation(sheet, comboArray, attr.prompt(), 1, 100, column, column); + } + } + } + + /** + * 添加单元格 + */ + public Cell addCell(Excel attr, Row row, T vo, Field field, int column) + { + Cell cell = null; + try + { + // 设置行高 + row.setHeight(maxHeight); + // 根据Excel中设置情况决定是否导出,有些情况需要保持为空,希望用户填写这一列. + if (attr.isExport()) + { + // 创建cell + cell = row.createCell(column); + if (isSubListValue(vo) && getListCellValue(vo).size() > 1 && attr.needMerge()) + { + if (subMergedLastRowNum >= subMergedFirstRowNum) + { + sheet.addMergedRegion(new CellRangeAddress(subMergedFirstRowNum, subMergedLastRowNum, column, column)); + } + } + cell.setCellStyle(styles.get(StringUtils.format("data_{}_{}_{}_{}_{}", attr.align(), attr.color(), attr.backgroundColor(), attr.cellType(), attr.wrapText()))); + + // 用于读取对象中的属性 + Object value = getTargetValue(vo, field, attr); + String dateFormat = attr.dateFormat(); + String readConverterExp = attr.readConverterExp(); + String separator = attr.separator(); + String dictType = attr.dictType(); + if (StringUtils.isNotEmpty(dateFormat) && StringUtils.isNotNull(value)) + { + cell.getCellStyle().setDataFormat(this.wb.getCreationHelper().createDataFormat().getFormat(dateFormat)); + cell.setCellValue(parseDateToStr(dateFormat, value)); + } + else if (StringUtils.isNotEmpty(readConverterExp) && StringUtils.isNotNull(value)) + { + cell.setCellValue(convertByExp(Convert.toStr(value), readConverterExp, separator)); + } + else if (StringUtils.isNotEmpty(dictType) && StringUtils.isNotNull(value)) + { + if (!sysDictMap.containsKey(dictType + value)) + { + String lable = convertDictByExp(Convert.toStr(value), dictType, separator); + sysDictMap.put(dictType + value, lable); + } + cell.setCellValue(sysDictMap.get(dictType + value)); + } + else if (value instanceof BigDecimal && -1 != attr.scale()) + { + cell.setCellValue((((BigDecimal) value).setScale(attr.scale(), attr.roundingMode())).doubleValue()); + } + else if (!attr.handler().equals(ExcelHandlerAdapter.class)) + { + cell.setCellValue(dataFormatHandlerAdapter(value, attr, cell)); + } + else + { + // 设置列类型 + setCellVo(value, attr, cell); + } + addStatisticsData(column, Convert.toStr(value), attr); + } + } + catch (Exception e) + { + log.error("导出Excel失败{}", e); + } + return cell; + } + + /** + * 设置 POI XSSFSheet 单元格提示或选择框 + * + * @param sheet 表单 + * @param textlist 下拉框显示的内容 + * @param promptContent 提示内容 + * @param firstRow 开始行 + * @param endRow 结束行 + * @param firstCol 开始列 + * @param endCol 结束列 + */ + public void setPromptOrValidation(Sheet sheet, String[] textlist, String promptContent, int firstRow, int endRow, + int firstCol, int endCol) + { + DataValidationHelper helper = sheet.getDataValidationHelper(); + DataValidationConstraint constraint = textlist.length > 0 ? helper.createExplicitListConstraint(textlist) : helper.createCustomConstraint("DD1"); + CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol); + DataValidation dataValidation = helper.createValidation(constraint, regions); + if (StringUtils.isNotEmpty(promptContent)) + { + // 如果设置了提示信息则鼠标放上去提示 + dataValidation.createPromptBox("", promptContent); + dataValidation.setShowPromptBox(true); + } + // 处理Excel兼容性问题 + if (dataValidation instanceof XSSFDataValidation) + { + dataValidation.setSuppressDropDownArrow(true); + dataValidation.setShowErrorBox(true); + } + else + { + dataValidation.setSuppressDropDownArrow(false); + } + sheet.addValidationData(dataValidation); + } + + /** + * 设置某些列的值只能输入预制的数据,显示下拉框(兼容超出一定数量的下拉框). + * + * @param sheet 要设置的sheet. + * @param textlist 下拉框显示的内容 + * @param promptContent 提示内容 + * @param firstRow 开始行 + * @param endRow 结束行 + * @param firstCol 开始列 + * @param endCol 结束列 + */ + public void setXSSFValidationWithHidden(Sheet sheet, String[] textlist, String promptContent, int firstRow, int endRow, int firstCol, int endCol) + { + String hideSheetName = "combo_" + firstCol + "_" + endCol; + Sheet hideSheet = wb.createSheet(hideSheetName); // 用于存储 下拉菜单数据 + for (int i = 0; i < textlist.length; i++) + { + hideSheet.createRow(i).createCell(0).setCellValue(textlist[i]); + } + // 创建名称,可被其他单元格引用 + Name name = wb.createName(); + name.setNameName(hideSheetName + "_data"); + name.setRefersToFormula(hideSheetName + "!$A$1:$A$" + textlist.length); + DataValidationHelper helper = sheet.getDataValidationHelper(); + // 加载下拉列表内容 + DataValidationConstraint constraint = helper.createFormulaListConstraint(hideSheetName + "_data"); + // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列 + CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol); + // 数据有效性对象 + DataValidation dataValidation = helper.createValidation(constraint, regions); + if (StringUtils.isNotEmpty(promptContent)) + { + // 如果设置了提示信息则鼠标放上去提示 + dataValidation.createPromptBox("", promptContent); + dataValidation.setShowPromptBox(true); + } + // 处理Excel兼容性问题 + if (dataValidation instanceof XSSFDataValidation) + { + dataValidation.setSuppressDropDownArrow(true); + dataValidation.setShowErrorBox(true); + } + else + { + dataValidation.setSuppressDropDownArrow(false); + } + + sheet.addValidationData(dataValidation); + // 设置hiddenSheet隐藏 + wb.setSheetHidden(wb.getSheetIndex(hideSheet), true); + } + + /** + * 解析导出值 0=男,1=女,2=未知 + * + * @param propertyValue 参数值 + * @param converterExp 翻译注解 + * @param separator 分隔符 + * @return 解析后值 + */ + public static String convertByExp(String propertyValue, String converterExp, String separator) + { + StringBuilder propertyString = new StringBuilder(); + String[] convertSource = converterExp.split(SEPARATOR); + for (String item : convertSource) + { + String[] itemArray = item.split("="); + if (StringUtils.containsAny(propertyValue, separator)) + { + for (String value : propertyValue.split(separator)) + { + if (itemArray[0].equals(value)) + { + propertyString.append(itemArray[1] + separator); + break; + } + } + } + else + { + if (itemArray[0].equals(propertyValue)) + { + return itemArray[1]; + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 反向解析值 男=0,女=1,未知=2 + * + * @param propertyValue 参数值 + * @param converterExp 翻译注解 + * @param separator 分隔符 + * @return 解析后值 + */ + public static String reverseByExp(String propertyValue, String converterExp, String separator) + { + StringBuilder propertyString = new StringBuilder(); + String[] convertSource = converterExp.split(SEPARATOR); + for (String item : convertSource) + { + String[] itemArray = item.split("="); + if (StringUtils.containsAny(propertyValue, separator)) + { + for (String value : propertyValue.split(separator)) + { + if (itemArray[1].equals(value)) + { + propertyString.append(itemArray[0] + separator); + break; + } + } + } + else + { + if (itemArray[1].equals(propertyValue)) + { + return itemArray[0]; + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 解析字典值 + * + * @param dictValue 字典值 + * @param dictType 字典类型 + * @param separator 分隔符 + * @return 字典标签 + */ + public static String convertDictByExp(String dictValue, String dictType, String separator) + { + return DictUtils.getDictLabel(dictType, dictValue, separator); + } + + /** + * 反向解析值字典值 + * + * @param dictLabel 字典标签 + * @param dictType 字典类型 + * @param separator 分隔符 + * @return 字典值 + */ + public static String reverseDictByExp(String dictLabel, String dictType, String separator) + { + return DictUtils.getDictValue(dictType, dictLabel, separator); + } + + /** + * 数据处理器 + * + * @param value 数据值 + * @param excel 数据注解 + * @return + */ + public String dataFormatHandlerAdapter(Object value, Excel excel, Cell cell) + { + try + { + Object instance = excel.handler().newInstance(); + Method formatMethod = excel.handler().getMethod("format", new Class[] { Object.class, String[].class, Cell.class, Workbook.class }); + value = formatMethod.invoke(instance, value, excel.args(), cell, this.wb); + } + catch (Exception e) + { + log.error("不能格式化数据 " + excel.handler(), e.getMessage()); + } + return Convert.toStr(value); + } + + /** + * 合计统计信息 + */ + private void addStatisticsData(Integer index, String text, Excel entity) + { + if (entity != null && entity.isStatistics()) + { + Double temp = 0D; + if (!statistics.containsKey(index)) + { + statistics.put(index, temp); + } + try + { + temp = Double.valueOf(text); + } + catch (NumberFormatException e) + { + } + statistics.put(index, statistics.get(index) + temp); + } + } + + /** + * 创建统计行 + */ + public void addStatisticsRow() + { + if (statistics.size() > 0) + { + Row row = sheet.createRow(sheet.getLastRowNum() + 1); + Set keys = statistics.keySet(); + Cell cell = row.createCell(0); + cell.setCellStyle(styles.get("total")); + cell.setCellValue("合计"); + + for (Integer key : keys) + { + cell = row.createCell(key); + cell.setCellStyle(styles.get("total")); + cell.setCellValue(statistics.get(key)); + } + statistics.clear(); + } + } + + /** + * 编码文件名 + */ + public String encodingFilename(String filename) + { + return UUID.randomUUID() + "_" + filename + ".xlsx"; + } + + /** + * 获取下载路径 + * + * @param filename 文件名称 + */ + public String getAbsoluteFile(String filename) + { + String downloadPath = RuoYiConfig.getDownloadPath() + filename; + File desc = new File(downloadPath); + if (!desc.getParentFile().exists()) + { + desc.getParentFile().mkdirs(); + } + return downloadPath; + } + + /** + * 获取bean中的属性值 + * + * @param vo 实体对象 + * @param field 字段 + * @param excel 注解 + * @return 最终的属性值 + * @throws Exception + */ + private Object getTargetValue(T vo, Field field, Excel excel) throws Exception + { + field.setAccessible(true); + Object o = field.get(vo); + if (StringUtils.isNotEmpty(excel.targetAttr())) + { + String target = excel.targetAttr(); + if (target.contains(".")) + { + String[] targets = target.split("[.]"); + for (String name : targets) + { + o = getValue(o, name); + } + } + else + { + o = getValue(o, target); + } + } + return o; + } + + /** + * 以类的属性的get方法方法形式获取值 + * + * @param o + * @param name + * @return value + * @throws Exception + */ + private Object getValue(Object o, String name) throws Exception + { + if (StringUtils.isNotNull(o) && StringUtils.isNotEmpty(name)) + { + Class clazz = o.getClass(); + Field field = clazz.getDeclaredField(name); + field.setAccessible(true); + o = field.get(o); + } + return o; + } + + /** + * 得到所有定义字段 + */ + private void createExcelField() + { + this.fields = getFields(); + this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList()); + this.maxHeight = getRowHeight(); + } + + /** + * 获取字段注解信息 + */ + public List getFields() + { + List fields = new ArrayList(); + List tempFields = new ArrayList<>(); + tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields())); + tempFields.addAll(Arrays.asList(clazz.getDeclaredFields())); + if (StringUtils.isNotEmpty(includeFields)) + { + for (Field field : tempFields) + { + if (ArrayUtils.contains(this.includeFields, field.getName()) || field.isAnnotationPresent(Excels.class)) + { + addField(fields, field); + } + } + } + else if (StringUtils.isNotEmpty(excludeFields)) + { + for (Field field : tempFields) + { + if (!ArrayUtils.contains(this.excludeFields, field.getName())) + { + addField(fields, field); + } + } + } + else + { + for (Field field : tempFields) + { + addField(fields, field); + } + } + return fields; + } + + /** + * 添加字段信息 + */ + public void addField(List fields, Field field) + { + // 单注解 + if (field.isAnnotationPresent(Excel.class)) + { + Excel attr = field.getAnnotation(Excel.class); + if (attr != null && (attr.type() == Type.ALL || attr.type() == type)) + { + fields.add(new Object[] { field, attr }); + } + if (Collection.class.isAssignableFrom(field.getType())) + { + subMethod = getSubMethod(field.getName(), clazz); + ParameterizedType pt = (ParameterizedType) field.getGenericType(); + Class subClass = (Class) pt.getActualTypeArguments()[0]; + this.subFields = FieldUtils.getFieldsListWithAnnotation(subClass, Excel.class); + } + } + + // 多注解 + if (field.isAnnotationPresent(Excels.class)) + { + Excels attrs = field.getAnnotation(Excels.class); + Excel[] excels = attrs.value(); + for (Excel attr : excels) + { + if (StringUtils.isNotEmpty(includeFields)) + { + if (ArrayUtils.contains(this.includeFields, field.getName() + "." + attr.targetAttr()) + && (attr != null && (attr.type() == Type.ALL || attr.type() == type))) + { + fields.add(new Object[] { field, attr }); + } + } + else + { + if (!ArrayUtils.contains(this.excludeFields, field.getName() + "." + attr.targetAttr()) + && (attr != null && (attr.type() == Type.ALL || attr.type() == type))) + { + fields.add(new Object[] { field, attr }); + } + } + } + } + } + + /** + * 根据注解获取最大行高 + */ + public short getRowHeight() + { + double maxHeight = 0; + for (Object[] os : this.fields) + { + Excel excel = (Excel) os[1]; + maxHeight = Math.max(maxHeight, excel.height()); + } + return (short) (maxHeight * 20); + } + + /** + * 创建一个工作簿 + */ + public void createWorkbook() + { + this.wb = new SXSSFWorkbook(500); + this.sheet = wb.createSheet(); + wb.setSheetName(0, sheetName); + this.styles = createStyles(wb); + } + + /** + * 创建工作表 + * + * @param sheetNo sheet数量 + * @param index 序号 + */ + public void createSheet(int sheetNo, int index) + { + // 设置工作表的名称. + if (sheetNo > 1 && index > 0) + { + this.sheet = wb.createSheet(); + this.createTitle(); + wb.setSheetName(index, sheetName + index); + } + } + + /** + * 获取单元格值 + * + * @param row 获取的行 + * @param column 获取单元格列号 + * @return 单元格值 + */ + public Object getCellValue(Row row, int column) + { + if (row == null) + { + return row; + } + Object val = ""; + try + { + Cell cell = row.getCell(column); + if (StringUtils.isNotNull(cell)) + { + if (cell.getCellType() == CellType.NUMERIC || cell.getCellType() == CellType.FORMULA) + { + val = cell.getNumericCellValue(); + if (DateUtil.isCellDateFormatted(cell)) + { + val = DateUtil.getJavaDate((Double) val); // POI Excel 日期格式转换 + } + else + { + if ((Double) val % 1 != 0) + { + val = new BigDecimal(val.toString()); + } + else + { + val = new DecimalFormat("0").format(val); + } + } + } + else if (cell.getCellType() == CellType.STRING) + { + val = cell.getStringCellValue(); + } + else if (cell.getCellType() == CellType.BOOLEAN) + { + val = cell.getBooleanCellValue(); + } + else if (cell.getCellType() == CellType.ERROR) + { + val = cell.getErrorCellValue(); + } + + } + } + catch (Exception e) + { + return val; + } + return val; + } + + /** + * 判断是否是空行 + * + * @param row 判断的行 + * @return + */ + private boolean isRowEmpty(Row row) + { + if (row == null) + { + return true; + } + for (int i = row.getFirstCellNum(); i < row.getLastCellNum(); i++) + { + Cell cell = row.getCell(i); + if (cell != null && cell.getCellType() != CellType.BLANK) + { + return false; + } + } + return true; + } + + /** + * 获取Excel2003图片 + * + * @param sheet 当前sheet对象 + * @param workbook 工作簿对象 + * @return Map key:图片单元格索引(1_1)String,value:图片流PictureData + */ + public static Map> getSheetPictures03(HSSFSheet sheet, HSSFWorkbook workbook) + { + Map> sheetIndexPicMap = new HashMap<>(); + List pictures = workbook.getAllPictures(); + if (!pictures.isEmpty() && sheet.getDrawingPatriarch() != null) + { + for (HSSFShape shape : sheet.getDrawingPatriarch().getChildren()) + { + if (shape instanceof HSSFPicture) + { + HSSFPicture pic = (HSSFPicture) shape; + HSSFClientAnchor anchor = (HSSFClientAnchor) pic.getAnchor(); + String picIndex = anchor.getRow1() + "_" + anchor.getCol1(); + sheetIndexPicMap.computeIfAbsent(picIndex, k -> new ArrayList<>()).add(pic.getPictureData()); + } + } + } + return sheetIndexPicMap; + } + + /** + * 获取Excel2007图片 + * + * @param sheet 当前sheet对象 + * @param workbook 工作簿对象 + * @return Map key:图片单元格索引(1_1)String,value:图片流PictureData + */ + public static Map> getSheetPictures07(XSSFSheet sheet, XSSFWorkbook workbook) + { + Map> sheetIndexPicMap = new HashMap<>(); + for (POIXMLDocumentPart dr : sheet.getRelations()) + { + if (dr instanceof XSSFDrawing) + { + XSSFDrawing drawing = (XSSFDrawing) dr; + for (XSSFShape shape : drawing.getShapes()) + { + if (shape instanceof XSSFPicture) + { + XSSFPicture pic = (XSSFPicture) shape; + XSSFClientAnchor anchor = pic.getPreferredSize(); + CTMarker ctMarker = anchor.getFrom(); + String picIndex = ctMarker.getRow() + "_" + ctMarker.getCol(); + sheetIndexPicMap.computeIfAbsent(picIndex, k -> new ArrayList<>()).add(pic.getPictureData()); + } + } + } + } + return sheetIndexPicMap; + } + + /** + * 格式化不同类型的日期对象 + * + * @param dateFormat 日期格式 + * @param val 被格式化的日期对象 + * @return 格式化后的日期字符 + */ + public String parseDateToStr(String dateFormat, Object val) + { + if (val == null) + { + return ""; + } + String str; + if (val instanceof Date) + { + str = DateUtils.parseDateToStr(dateFormat, (Date) val); + } + else if (val instanceof LocalDateTime) + { + str = DateUtils.parseDateToStr(dateFormat, DateUtils.toDate((LocalDateTime) val)); + } + else if (val instanceof LocalDate) + { + str = DateUtils.parseDateToStr(dateFormat, DateUtils.toDate((LocalDate) val)); + } + else + { + str = val.toString(); + } + return str; + } + + /** + * 是否有对象的子列表 + */ + public boolean isSubList() + { + return StringUtils.isNotNull(subFields) && subFields.size() > 0; + } + + /** + * 是否有对象的子列表,集合不为空 + */ + public boolean isSubListValue(T vo) + { + return StringUtils.isNotNull(subFields) && subFields.size() > 0 && StringUtils.isNotNull(getListCellValue(vo)) && getListCellValue(vo).size() > 0; + } + + /** + * 获取集合的值 + */ + public Collection getListCellValue(Object obj) + { + Object value; + try + { + value = subMethod.invoke(obj, new Object[] {}); + } + catch (Exception e) + { + return new ArrayList(); + } + return (Collection) value; + } + + /** + * 获取对象的子列表方法 + * + * @param name 名称 + * @param pojoClass 类对象 + * @return 子列表方法 + */ + public Method getSubMethod(String name, Class pojoClass) + { + StringBuffer getMethodName = new StringBuffer("get"); + getMethodName.append(name.substring(0, 1).toUpperCase()); + getMethodName.append(name.substring(1)); + Method method = null; + try + { + method = pojoClass.getMethod(getMethodName.toString(), new Class[] {}); + } + catch (Exception e) + { + log.error("获取对象异常{}", e.getMessage()); + } + return method; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/reflect/ReflectUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/reflect/ReflectUtils.java new file mode 100644 index 0000000..b19953e --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/reflect/ReflectUtils.java @@ -0,0 +1,410 @@ +package com.ruoyi.common.utils.reflect; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Date; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.poi.ss.usermodel.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.utils.DateUtils; + +/** + * 反射工具类. 提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数. + * + * @author ruoyi + */ +@SuppressWarnings("rawtypes") +public class ReflectUtils +{ + private static final String SETTER_PREFIX = "set"; + + private static final String GETTER_PREFIX = "get"; + + private static final String CGLIB_CLASS_SEPARATOR = "$$"; + + private static Logger logger = LoggerFactory.getLogger(ReflectUtils.class); + + /** + * 调用Getter方法. + * 支持多级,如:对象名.对象名.方法 + */ + @SuppressWarnings("unchecked") + public static E invokeGetter(Object obj, String propertyName) + { + Object object = obj; + for (String name : StringUtils.split(propertyName, ".")) + { + String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(name); + object = invokeMethod(object, getterMethodName, new Class[] {}, new Object[] {}); + } + return (E) object; + } + + /** + * 调用Setter方法, 仅匹配方法名。 + * 支持多级,如:对象名.对象名.方法 + */ + public static void invokeSetter(Object obj, String propertyName, E value) + { + Object object = obj; + String[] names = StringUtils.split(propertyName, "."); + for (int i = 0; i < names.length; i++) + { + if (i < names.length - 1) + { + String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(names[i]); + object = invokeMethod(object, getterMethodName, new Class[] {}, new Object[] {}); + } + else + { + String setterMethodName = SETTER_PREFIX + StringUtils.capitalize(names[i]); + invokeMethodByName(object, setterMethodName, new Object[] { value }); + } + } + } + + /** + * 直接读取对象属性值, 无视private/protected修饰符, 不经过getter函数. + */ + @SuppressWarnings("unchecked") + public static E getFieldValue(final Object obj, final String fieldName) + { + Field field = getAccessibleField(obj, fieldName); + if (field == null) + { + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 "); + return null; + } + E result = null; + try + { + result = (E) field.get(obj); + } + catch (IllegalAccessException e) + { + logger.error("不可能抛出的异常{}", e.getMessage()); + } + return result; + } + + /** + * 直接设置对象属性值, 无视private/protected修饰符, 不经过setter函数. + */ + public static void setFieldValue(final Object obj, final String fieldName, final E value) + { + Field field = getAccessibleField(obj, fieldName); + if (field == null) + { + // throw new IllegalArgumentException("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 "); + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 "); + return; + } + try + { + field.set(obj, value); + } + catch (IllegalAccessException e) + { + logger.error("不可能抛出的异常: {}", e.getMessage()); + } + } + + /** + * 直接调用对象方法, 无视private/protected修饰符. + * 用于一次性调用的情况,否则应使用getAccessibleMethod()函数获得Method后反复调用. + * 同时匹配方法名+参数类型, + */ + @SuppressWarnings("unchecked") + public static E invokeMethod(final Object obj, final String methodName, final Class[] parameterTypes, + final Object[] args) + { + if (obj == null || methodName == null) + { + return null; + } + Method method = getAccessibleMethod(obj, methodName, parameterTypes); + if (method == null) + { + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 "); + return null; + } + try + { + return (E) method.invoke(obj, args); + } + catch (Exception e) + { + String msg = "method: " + method + ", obj: " + obj + ", args: " + args + ""; + throw convertReflectionExceptionToUnchecked(msg, e); + } + } + + /** + * 直接调用对象方法, 无视private/protected修饰符, + * 用于一次性调用的情况,否则应使用getAccessibleMethodByName()函数获得Method后反复调用. + * 只匹配函数名,如果有多个同名函数调用第一个。 + */ + @SuppressWarnings("unchecked") + public static E invokeMethodByName(final Object obj, final String methodName, final Object[] args) + { + Method method = getAccessibleMethodByName(obj, methodName, args.length); + if (method == null) + { + // 如果为空不报错,直接返回空。 + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 "); + return null; + } + try + { + // 类型转换(将参数数据类型转换为目标方法参数类型) + Class[] cs = method.getParameterTypes(); + for (int i = 0; i < cs.length; i++) + { + if (args[i] != null && !args[i].getClass().equals(cs[i])) + { + if (cs[i] == String.class) + { + args[i] = Convert.toStr(args[i]); + if (StringUtils.endsWith((String) args[i], ".0")) + { + args[i] = StringUtils.substringBefore((String) args[i], ".0"); + } + } + else if (cs[i] == Integer.class) + { + args[i] = Convert.toInt(args[i]); + } + else if (cs[i] == Long.class) + { + args[i] = Convert.toLong(args[i]); + } + else if (cs[i] == Double.class) + { + args[i] = Convert.toDouble(args[i]); + } + else if (cs[i] == Float.class) + { + args[i] = Convert.toFloat(args[i]); + } + else if (cs[i] == Date.class) + { + if (args[i] instanceof String) + { + args[i] = DateUtils.parseDate(args[i]); + } + else + { + args[i] = DateUtil.getJavaDate((Double) args[i]); + } + } + else if (cs[i] == boolean.class || cs[i] == Boolean.class) + { + args[i] = Convert.toBool(args[i]); + } + } + } + return (E) method.invoke(obj, args); + } + catch (Exception e) + { + String msg = "method: " + method + ", obj: " + obj + ", args: " + args + ""; + throw convertReflectionExceptionToUnchecked(msg, e); + } + } + + /** + * 循环向上转型, 获取对象的DeclaredField, 并强制设置为可访问. + * 如向上转型到Object仍无法找到, 返回null. + */ + public static Field getAccessibleField(final Object obj, final String fieldName) + { + // 为空不报错。直接返回 null + if (obj == null) + { + return null; + } + Validate.notBlank(fieldName, "fieldName can't be blank"); + for (Class superClass = obj.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) + { + try + { + Field field = superClass.getDeclaredField(fieldName); + makeAccessible(field); + return field; + } + catch (NoSuchFieldException e) + { + continue; + } + } + return null; + } + + /** + * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问. + * 如向上转型到Object仍无法找到, 返回null. + * 匹配函数名+参数类型。 + * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args) + */ + public static Method getAccessibleMethod(final Object obj, final String methodName, + final Class... parameterTypes) + { + // 为空不报错。直接返回 null + if (obj == null) + { + return null; + } + Validate.notBlank(methodName, "methodName can't be blank"); + for (Class searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass()) + { + try + { + Method method = searchType.getDeclaredMethod(methodName, parameterTypes); + makeAccessible(method); + return method; + } + catch (NoSuchMethodException e) + { + continue; + } + } + return null; + } + + /** + * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问. + * 如向上转型到Object仍无法找到, 返回null. + * 只匹配函数名。 + * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args) + */ + public static Method getAccessibleMethodByName(final Object obj, final String methodName, int argsNum) + { + // 为空不报错。直接返回 null + if (obj == null) + { + return null; + } + Validate.notBlank(methodName, "methodName can't be blank"); + for (Class searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass()) + { + Method[] methods = searchType.getDeclaredMethods(); + for (Method method : methods) + { + if (method.getName().equals(methodName) && method.getParameterTypes().length == argsNum) + { + makeAccessible(method); + return method; + } + } + } + return null; + } + + /** + * 改变private/protected的方法为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。 + */ + public static void makeAccessible(Method method) + { + if ((!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers())) + && !method.isAccessible()) + { + method.setAccessible(true); + } + } + + /** + * 改变private/protected的成员变量为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。 + */ + public static void makeAccessible(Field field) + { + if ((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers()) + || Modifier.isFinal(field.getModifiers())) && !field.isAccessible()) + { + field.setAccessible(true); + } + } + + /** + * 通过反射, 获得Class定义中声明的泛型参数的类型, 注意泛型必须定义在父类处 + * 如无法找到, 返回Object.class. + */ + @SuppressWarnings("unchecked") + public static Class getClassGenricType(final Class clazz) + { + return getClassGenricType(clazz, 0); + } + + /** + * 通过反射, 获得Class定义中声明的父类的泛型参数的类型. + * 如无法找到, 返回Object.class. + */ + public static Class getClassGenricType(final Class clazz, final int index) + { + Type genType = clazz.getGenericSuperclass(); + + if (!(genType instanceof ParameterizedType)) + { + logger.debug(clazz.getSimpleName() + "'s superclass not ParameterizedType"); + return Object.class; + } + + Type[] params = ((ParameterizedType) genType).getActualTypeArguments(); + + if (index >= params.length || index < 0) + { + logger.debug("Index: " + index + ", Size of " + clazz.getSimpleName() + "'s Parameterized Type: " + + params.length); + return Object.class; + } + if (!(params[index] instanceof Class)) + { + logger.debug(clazz.getSimpleName() + " not set the actual class on superclass generic parameter"); + return Object.class; + } + + return (Class) params[index]; + } + + public static Class getUserClass(Object instance) + { + if (instance == null) + { + throw new RuntimeException("Instance must not be null"); + } + Class clazz = instance.getClass(); + if (clazz != null && clazz.getName().contains(CGLIB_CLASS_SEPARATOR)) + { + Class superClass = clazz.getSuperclass(); + if (superClass != null && !Object.class.equals(superClass)) + { + return superClass; + } + } + return clazz; + + } + + /** + * 将反射时的checked exception转换为unchecked exception. + */ + public static RuntimeException convertReflectionExceptionToUnchecked(String msg, Exception e) + { + if (e instanceof IllegalAccessException || e instanceof IllegalArgumentException + || e instanceof NoSuchMethodException) + { + return new IllegalArgumentException(msg, e); + } + else if (e instanceof InvocationTargetException) + { + return new RuntimeException(msg, ((InvocationTargetException) e).getTargetException()); + } + return new RuntimeException(msg, e); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Base64.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Base64.java new file mode 100644 index 0000000..ca1cd92 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Base64.java @@ -0,0 +1,291 @@ +package com.ruoyi.common.utils.sign; + +/** + * Base64工具类 + * + * @author ruoyi + */ +public final class Base64 +{ + static private final int BASELENGTH = 128; + static private final int LOOKUPLENGTH = 64; + static private final int TWENTYFOURBITGROUP = 24; + static private final int EIGHTBIT = 8; + static private final int SIXTEENBIT = 16; + static private final int FOURBYTE = 4; + static private final int SIGN = -128; + static private final char PAD = '='; + static final private byte[] base64Alphabet = new byte[BASELENGTH]; + static final private char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH]; + + static + { + for (int i = 0; i < BASELENGTH; ++i) + { + base64Alphabet[i] = -1; + } + for (int i = 'Z'; i >= 'A'; i--) + { + base64Alphabet[i] = (byte) (i - 'A'); + } + for (int i = 'z'; i >= 'a'; i--) + { + base64Alphabet[i] = (byte) (i - 'a' + 26); + } + + for (int i = '9'; i >= '0'; i--) + { + base64Alphabet[i] = (byte) (i - '0' + 52); + } + + base64Alphabet['+'] = 62; + base64Alphabet['/'] = 63; + + for (int i = 0; i <= 25; i++) + { + lookUpBase64Alphabet[i] = (char) ('A' + i); + } + + for (int i = 26, j = 0; i <= 51; i++, j++) + { + lookUpBase64Alphabet[i] = (char) ('a' + j); + } + + for (int i = 52, j = 0; i <= 61; i++, j++) + { + lookUpBase64Alphabet[i] = (char) ('0' + j); + } + lookUpBase64Alphabet[62] = (char) '+'; + lookUpBase64Alphabet[63] = (char) '/'; + } + + private static boolean isWhiteSpace(char octect) + { + return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9); + } + + private static boolean isPad(char octect) + { + return (octect == PAD); + } + + private static boolean isData(char octect) + { + return (octect < BASELENGTH && base64Alphabet[octect] != -1); + } + + /** + * Encodes hex octects into Base64 + * + * @param binaryData Array containing binaryData + * @return Encoded Base64 array + */ + public static String encode(byte[] binaryData) + { + if (binaryData == null) + { + return null; + } + + int lengthDataBits = binaryData.length * EIGHTBIT; + if (lengthDataBits == 0) + { + return ""; + } + + int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP; + int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP; + int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 : numberTriplets; + char encodedData[] = null; + + encodedData = new char[numberQuartet * 4]; + + byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0; + + int encodedIndex = 0; + int dataIndex = 0; + + for (int i = 0; i < numberTriplets; i++) + { + b1 = binaryData[dataIndex++]; + b2 = binaryData[dataIndex++]; + b3 = binaryData[dataIndex++]; + + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f]; + } + + // form integral number of 6-bit groups + if (fewerThan24bits == EIGHTBIT) + { + b1 = binaryData[dataIndex]; + k = (byte) (b1 & 0x03); + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4]; + encodedData[encodedIndex++] = PAD; + encodedData[encodedIndex++] = PAD; + } + else if (fewerThan24bits == SIXTEENBIT) + { + b1 = binaryData[dataIndex]; + b2 = binaryData[dataIndex + 1]; + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2]; + encodedData[encodedIndex++] = PAD; + } + return new String(encodedData); + } + + /** + * Decodes Base64 data into octects + * + * @param encoded string containing Base64 data + * @return Array containind decoded data. + */ + public static byte[] decode(String encoded) + { + if (encoded == null) + { + return null; + } + + char[] base64Data = encoded.toCharArray(); + // remove white spaces + int len = removeWhiteSpace(base64Data); + + if (len % FOURBYTE != 0) + { + return null;// should be divisible by four + } + + int numberQuadruple = (len / FOURBYTE); + + if (numberQuadruple == 0) + { + return new byte[0]; + } + + byte decodedData[] = null; + byte b1 = 0, b2 = 0, b3 = 0, b4 = 0; + char d1 = 0, d2 = 0, d3 = 0, d4 = 0; + + int i = 0; + int encodedIndex = 0; + int dataIndex = 0; + decodedData = new byte[(numberQuadruple) * 3]; + + for (; i < numberQuadruple - 1; i++) + { + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++])) + || !isData((d3 = base64Data[dataIndex++])) || !isData((d4 = base64Data[dataIndex++]))) + { + return null; + } // if found "no data" just return null + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + } + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))) + { + return null;// if found "no data" just return null + } + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + + d3 = base64Data[dataIndex++]; + d4 = base64Data[dataIndex++]; + if (!isData((d3)) || !isData((d4))) + {// Check if they are PAD characters + if (isPad(d3) && isPad(d4)) + { + if ((b2 & 0xf) != 0)// last 4 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 1]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); + return tmp; + } + else if (!isPad(d3) && isPad(d4)) + { + b3 = base64Alphabet[d3]; + if ((b3 & 0x3) != 0)// last 2 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 2]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + return tmp; + } + else + { + return null; + } + } + else + { // No PAD e.g 3cQl + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + + } + return decodedData; + } + + /** + * remove WhiteSpace from MIME containing encoded Base64 data. + * + * @param data the byte array of base64 data (with WS) + * @return the new length + */ + private static int removeWhiteSpace(char[] data) + { + if (data == null) + { + return 0; + } + + // count characters that's not whitespace + int newSize = 0; + int len = data.length; + for (int i = 0; i < len; i++) + { + if (!isWhiteSpace(data[i])) + { + data[newSize++] = data[i]; + } + } + return newSize; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Md5Utils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Md5Utils.java new file mode 100644 index 0000000..c1c58db --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sign/Md5Utils.java @@ -0,0 +1,67 @@ +package com.ruoyi.common.utils.sign; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Md5加密方法 + * + * @author ruoyi + */ +public class Md5Utils +{ + private static final Logger log = LoggerFactory.getLogger(Md5Utils.class); + + private static byte[] md5(String s) + { + MessageDigest algorithm; + try + { + algorithm = MessageDigest.getInstance("MD5"); + algorithm.reset(); + algorithm.update(s.getBytes("UTF-8")); + byte[] messageDigest = algorithm.digest(); + return messageDigest; + } + catch (Exception e) + { + log.error("MD5 Error...", e); + } + return null; + } + + private static final String toHex(byte hash[]) + { + if (hash == null) + { + return null; + } + StringBuffer buf = new StringBuffer(hash.length * 2); + int i; + + for (i = 0; i < hash.length; i++) + { + if ((hash[i] & 0xff) < 0x10) + { + buf.append("0"); + } + buf.append(Long.toString(hash[i] & 0xff, 16)); + } + return buf.toString(); + } + + public static String hash(String s) + { + try + { + return new String(toHex(md5(s)).getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); + } + catch (Exception e) + { + log.error("not supported charset...{}", e); + return s; + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/spring/SpringUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/spring/SpringUtils.java new file mode 100644 index 0000000..4e3f603 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/spring/SpringUtils.java @@ -0,0 +1,164 @@ +package com.ruoyi.common.utils.spring; + +import org.springframework.aop.framework.Advised; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; +import com.ruoyi.common.utils.StringUtils; + +/** + * spring工具类 方便在非spring管理环境中获取bean + * + * @author ruoyi + */ +@Component +public final class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware +{ + /** Spring应用上下文环境 */ + private static ConfigurableListableBeanFactory beanFactory; + + private static ApplicationContext applicationContext; + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException + { + SpringUtils.beanFactory = beanFactory; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException + { + SpringUtils.applicationContext = applicationContext; + } + + /** + * 获取对象 + * + * @param name + * @return Object 一个以所给名字注册的bean的实例 + * @throws org.springframework.beans.BeansException + * + */ + @SuppressWarnings("unchecked") + public static T getBean(String name) throws BeansException + { + return (T) beanFactory.getBean(name); + } + + /** + * 获取类型为requiredType的对象 + * + * @param clz + * @return + * @throws org.springframework.beans.BeansException + * + */ + public static T getBean(Class clz) throws BeansException + { + T result = (T) beanFactory.getBean(clz); + return result; + } + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + * + * @param name + * @return boolean + */ + public static boolean containsBean(String name) + { + return beanFactory.containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + * + * @param name + * @return boolean + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + * + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.isSingleton(name); + } + + /** + * @param name + * @return Class 注册对象的类型 + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + * + */ + public static Class getType(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + * + * @param name + * @return + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + * + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.getAliases(name); + } + + /** + * 获取aop代理对象 + * + * @param invoker + * @return + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) + { + Object proxy = AopContext.currentProxy(); + if (((Advised) proxy).getTargetSource().getTargetClass() == invoker.getClass()) + { + return (T) proxy; + } + return invoker; + } + + /** + * 获取当前的环境配置,无配置返回null + * + * @return 当前的环境配置 + */ + public static String[] getActiveProfiles() + { + return applicationContext.getEnvironment().getActiveProfiles(); + } + + /** + * 获取当前的环境配置,当有多个环境配置时,只获取第一个 + * + * @return 当前的环境配置 + */ + public static String getActiveProfile() + { + final String[] activeProfiles = getActiveProfiles(); + return StringUtils.isNotEmpty(activeProfiles) ? activeProfiles[0] : null; + } + + /** + * 获取配置文件中的值 + * + * @param key 配置文件的key + * @return 当前的配置文件的值 + * + */ + public static String getRequiredProperty(String key) + { + return applicationContext.getEnvironment().getRequiredProperty(key); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/sql/SqlUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sql/SqlUtil.java new file mode 100644 index 0000000..48720dc --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/sql/SqlUtil.java @@ -0,0 +1,70 @@ +package com.ruoyi.common.utils.sql; + +import com.ruoyi.common.exception.UtilException; +import com.ruoyi.common.utils.StringUtils; + +/** + * sql操作工具类 + * + * @author ruoyi + */ +public class SqlUtil +{ + /** + * 定义常用的 sql关键字 + */ + public static String SQL_REGEX = "\u000B|and |extractvalue|updatexml|sleep|exec |insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |or |union |like |+|/*|user()"; + + /** + * 仅支持字母、数字、下划线、空格、逗号、小数点(支持多个字段排序) + */ + public static String SQL_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+"; + + /** + * 限制orderBy最大长度 + */ + private static final int ORDER_BY_MAX_LENGTH = 500; + + /** + * 检查字符,防止注入绕过 + */ + public static String escapeOrderBySql(String value) + { + if (StringUtils.isNotEmpty(value) && !isValidOrderBySql(value)) + { + throw new UtilException("参数不符合规范,不能进行查询"); + } + if (StringUtils.length(value) > ORDER_BY_MAX_LENGTH) + { + throw new UtilException("参数已超过最大限制,不能进行查询"); + } + return value; + } + + /** + * 验证 order by 语法是否符合规范 + */ + public static boolean isValidOrderBySql(String value) + { + return value.matches(SQL_PATTERN); + } + + /** + * SQL关键字检查 + */ + public static void filterKeyword(String value) + { + if (StringUtils.isEmpty(value)) + { + return; + } + String[] sqlKeywords = StringUtils.split(SQL_REGEX, "\\|"); + for (String sqlKeyword : sqlKeywords) + { + if (StringUtils.indexOfIgnoreCase(value, sqlKeyword) > -1) + { + throw new UtilException("参数存在SQL注入风险"); + } + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/IdUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/IdUtils.java new file mode 100644 index 0000000..2c84427 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/IdUtils.java @@ -0,0 +1,49 @@ +package com.ruoyi.common.utils.uuid; + +/** + * ID生成器工具类 + * + * @author ruoyi + */ +public class IdUtils +{ + /** + * 获取随机UUID + * + * @return 随机UUID + */ + public static String randomUUID() + { + return UUID.randomUUID().toString(); + } + + /** + * 简化的UUID,去掉了横线 + * + * @return 简化的UUID,去掉了横线 + */ + public static String simpleUUID() + { + return UUID.randomUUID().toString(true); + } + + /** + * 获取随机UUID,使用性能更好的ThreadLocalRandom生成UUID + * + * @return 随机UUID + */ + public static String fastUUID() + { + return UUID.fastUUID().toString(); + } + + /** + * 简化的UUID,去掉了横线,使用性能更好的ThreadLocalRandom生成UUID + * + * @return 简化的UUID,去掉了横线 + */ + public static String fastSimpleUUID() + { + return UUID.fastUUID().toString(true); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/Seq.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/Seq.java new file mode 100644 index 0000000..bf99611 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/Seq.java @@ -0,0 +1,86 @@ +package com.ruoyi.common.utils.uuid; + +import java.util.concurrent.atomic.AtomicInteger; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; + +/** + * @author ruoyi 序列生成类 + */ +public class Seq +{ + // 通用序列类型 + public static final String commSeqType = "COMMON"; + + // 上传序列类型 + public static final String uploadSeqType = "UPLOAD"; + + // 通用接口序列数 + private static AtomicInteger commSeq = new AtomicInteger(1); + + // 上传接口序列数 + private static AtomicInteger uploadSeq = new AtomicInteger(1); + + // 机器标识 + private static final String machineCode = "A"; + + /** + * 获取通用序列号 + * + * @return 序列值 + */ + public static String getId() + { + return getId(commSeqType); + } + + /** + * 默认16位序列号 yyMMddHHmmss + 一位机器标识 + 3长度循环递增字符串 + * + * @return 序列值 + */ + public static String getId(String type) + { + AtomicInteger atomicInt = commSeq; + if (uploadSeqType.equals(type)) + { + atomicInt = uploadSeq; + } + return getId(atomicInt, 3); + } + + /** + * 通用接口序列号 yyMMddHHmmss + 一位机器标识 + length长度循环递增字符串 + * + * @param atomicInt 序列数 + * @param length 数值长度 + * @return 序列值 + */ + public static String getId(AtomicInteger atomicInt, int length) + { + String result = DateUtils.dateTimeNow(); + result += machineCode; + result += getSeq(atomicInt, length); + return result; + } + + /** + * 序列循环递增字符串[1, 10 的 (length)幂次方), 用0左补齐length位数 + * + * @return 序列值 + */ + private synchronized static String getSeq(AtomicInteger atomicInt, int length) + { + // 先取值再+1 + int value = atomicInt.getAndIncrement(); + + // 如果更新后值>=10 的 (length)幂次方则重置为1 + int maxSeq = (int) Math.pow(10, length); + if (atomicInt.get() >= maxSeq) + { + atomicInt.set(1); + } + // 转字符串,用0左补齐 + return StringUtils.padl(value, length); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/UUID.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/UUID.java new file mode 100644 index 0000000..a5585d6 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/UUID.java @@ -0,0 +1,484 @@ +package com.ruoyi.common.utils.uuid; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; +import com.ruoyi.common.exception.UtilException; + +/** + * 提供通用唯一识别码(universally unique identifier)(UUID)实现 + * + * @author ruoyi + */ +public final class UUID implements java.io.Serializable, Comparable +{ + private static final long serialVersionUID = -1185015143654744140L; + + /** + * SecureRandom 的单例 + * + */ + private static class Holder + { + static final SecureRandom numberGenerator = getSecureRandom(); + } + + /** 此UUID的最高64有效位 */ + private final long mostSigBits; + + /** 此UUID的最低64有效位 */ + private final long leastSigBits; + + /** + * 私有构造 + * + * @param data 数据 + */ + private UUID(byte[] data) + { + long msb = 0; + long lsb = 0; + assert data.length == 16 : "data must be 16 bytes in length"; + for (int i = 0; i < 8; i++) + { + msb = (msb << 8) | (data[i] & 0xff); + } + for (int i = 8; i < 16; i++) + { + lsb = (lsb << 8) | (data[i] & 0xff); + } + this.mostSigBits = msb; + this.leastSigBits = lsb; + } + + /** + * 使用指定的数据构造新的 UUID。 + * + * @param mostSigBits 用于 {@code UUID} 的最高有效 64 位 + * @param leastSigBits 用于 {@code UUID} 的最低有效 64 位 + */ + public UUID(long mostSigBits, long leastSigBits) + { + this.mostSigBits = mostSigBits; + this.leastSigBits = leastSigBits; + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 + * + * @return 随机生成的 {@code UUID} + */ + public static UUID fastUUID() + { + return randomUUID(false); + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。 + * + * @return 随机生成的 {@code UUID} + */ + public static UUID randomUUID() + { + return randomUUID(true); + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。 + * + * @param isSecure 是否使用{@link SecureRandom}如果是可以获得更安全的随机码,否则可以得到更好的性能 + * @return 随机生成的 {@code UUID} + */ + public static UUID randomUUID(boolean isSecure) + { + final Random ng = isSecure ? Holder.numberGenerator : getRandom(); + + byte[] randomBytes = new byte[16]; + ng.nextBytes(randomBytes); + randomBytes[6] &= 0x0f; /* clear version */ + randomBytes[6] |= 0x40; /* set to version 4 */ + randomBytes[8] &= 0x3f; /* clear variant */ + randomBytes[8] |= 0x80; /* set to IETF variant */ + return new UUID(randomBytes); + } + + /** + * 根据指定的字节数组获取类型 3(基于名称的)UUID 的静态工厂。 + * + * @param name 用于构造 UUID 的字节数组。 + * + * @return 根据指定数组生成的 {@code UUID} + */ + public static UUID nameUUIDFromBytes(byte[] name) + { + MessageDigest md; + try + { + md = MessageDigest.getInstance("MD5"); + } + catch (NoSuchAlgorithmException nsae) + { + throw new InternalError("MD5 not supported"); + } + byte[] md5Bytes = md.digest(name); + md5Bytes[6] &= 0x0f; /* clear version */ + md5Bytes[6] |= 0x30; /* set to version 3 */ + md5Bytes[8] &= 0x3f; /* clear variant */ + md5Bytes[8] |= 0x80; /* set to IETF variant */ + return new UUID(md5Bytes); + } + + /** + * 根据 {@link #toString()} 方法中描述的字符串标准表示形式创建{@code UUID}。 + * + * @param name 指定 {@code UUID} 字符串 + * @return 具有指定值的 {@code UUID} + * @throws IllegalArgumentException 如果 name 与 {@link #toString} 中描述的字符串表示形式不符抛出此异常 + * + */ + public static UUID fromString(String name) + { + String[] components = name.split("-"); + if (components.length != 5) + { + throw new IllegalArgumentException("Invalid UUID string: " + name); + } + for (int i = 0; i < 5; i++) + { + components[i] = "0x" + components[i]; + } + + long mostSigBits = Long.decode(components[0]).longValue(); + mostSigBits <<= 16; + mostSigBits |= Long.decode(components[1]).longValue(); + mostSigBits <<= 16; + mostSigBits |= Long.decode(components[2]).longValue(); + + long leastSigBits = Long.decode(components[3]).longValue(); + leastSigBits <<= 48; + leastSigBits |= Long.decode(components[4]).longValue(); + + return new UUID(mostSigBits, leastSigBits); + } + + /** + * 返回此 UUID 的 128 位值中的最低有效 64 位。 + * + * @return 此 UUID 的 128 位值中的最低有效 64 位。 + */ + public long getLeastSignificantBits() + { + return leastSigBits; + } + + /** + * 返回此 UUID 的 128 位值中的最高有效 64 位。 + * + * @return 此 UUID 的 128 位值中最高有效 64 位。 + */ + public long getMostSignificantBits() + { + return mostSigBits; + } + + /** + * 与此 {@code UUID} 相关联的版本号. 版本号描述此 {@code UUID} 是如何生成的。 + *

+ * 版本号具有以下含意: + *

    + *
  • 1 基于时间的 UUID + *
  • 2 DCE 安全 UUID + *
  • 3 基于名称的 UUID + *
  • 4 随机生成的 UUID + *
+ * + * @return 此 {@code UUID} 的版本号 + */ + public int version() + { + // Version is bits masked by 0x000000000000F000 in MS long + return (int) ((mostSigBits >> 12) & 0x0f); + } + + /** + * 与此 {@code UUID} 相关联的变体号。变体号描述 {@code UUID} 的布局。 + *

+ * 变体号具有以下含意: + *

    + *
  • 0 为 NCS 向后兼容保留 + *
  • 2 IETF RFC 4122(Leach-Salz), 用于此类 + *
  • 6 保留,微软向后兼容 + *
  • 7 保留供以后定义使用 + *
+ * + * @return 此 {@code UUID} 相关联的变体号 + */ + public int variant() + { + // This field is composed of a varying number of bits. + // 0 - - Reserved for NCS backward compatibility + // 1 0 - The IETF aka Leach-Salz variant (used by this class) + // 1 1 0 Reserved, Microsoft backward compatibility + // 1 1 1 Reserved for future definition. + return (int) ((leastSigBits >>> (64 - (leastSigBits >>> 62))) & (leastSigBits >> 63)); + } + + /** + * 与此 UUID 相关联的时间戳值。 + * + *

+ * 60 位的时间戳值根据此 {@code UUID} 的 time_low、time_mid 和 time_hi 字段构造。
+ * 所得到的时间戳以 100 毫微秒为单位,从 UTC(通用协调时间) 1582 年 10 月 15 日零时开始。 + * + *

+ * 时间戳值仅在在基于时间的 UUID(其 version 类型为 1)中才有意义。
+ * 如果此 {@code UUID} 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。 + * + * @throws UnsupportedOperationException 如果此 {@code UUID} 不是 version 为 1 的 UUID。 + */ + public long timestamp() throws UnsupportedOperationException + { + checkTimeBase(); + return (mostSigBits & 0x0FFFL) << 48// + | ((mostSigBits >> 16) & 0x0FFFFL) << 32// + | mostSigBits >>> 32; + } + + /** + * 与此 UUID 相关联的时钟序列值。 + * + *

+ * 14 位的时钟序列值根据此 UUID 的 clock_seq 字段构造。clock_seq 字段用于保证在基于时间的 UUID 中的时间唯一性。 + *

+ * {@code clockSequence} 值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。 如果此 UUID 不是基于时间的 UUID,则此方法抛出 + * UnsupportedOperationException。 + * + * @return 此 {@code UUID} 的时钟序列 + * + * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1 + */ + public int clockSequence() throws UnsupportedOperationException + { + checkTimeBase(); + return (int) ((leastSigBits & 0x3FFF000000000000L) >>> 48); + } + + /** + * 与此 UUID 相关的节点值。 + * + *

+ * 48 位的节点值根据此 UUID 的 node 字段构造。此字段旨在用于保存机器的 IEEE 802 地址,该地址用于生成此 UUID 以保证空间唯一性。 + *

+ * 节点值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。
+ * 如果此 UUID 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。 + * + * @return 此 {@code UUID} 的节点值 + * + * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1 + */ + public long node() throws UnsupportedOperationException + { + checkTimeBase(); + return leastSigBits & 0x0000FFFFFFFFFFFFL; + } + + /** + * 返回此{@code UUID} 的字符串表现形式。 + * + *

+ * UUID 的字符串表示形式由此 BNF 描述: + * + *

+     * {@code
+     * UUID                   = ----
+     * time_low               = 4*
+     * time_mid               = 2*
+     * time_high_and_version  = 2*
+     * variant_and_sequence   = 2*
+     * node                   = 6*
+     * hexOctet               = 
+     * hexDigit               = [0-9a-fA-F]
+     * }
+     * 
+ * + * + * + * @return 此{@code UUID} 的字符串表现形式 + * @see #toString(boolean) + */ + @Override + public String toString() + { + return toString(false); + } + + /** + * 返回此{@code UUID} 的字符串表现形式。 + * + *

+ * UUID 的字符串表示形式由此 BNF 描述: + * + *

+     * {@code
+     * UUID                   = ----
+     * time_low               = 4*
+     * time_mid               = 2*
+     * time_high_and_version  = 2*
+     * variant_and_sequence   = 2*
+     * node                   = 6*
+     * hexOctet               = 
+     * hexDigit               = [0-9a-fA-F]
+     * }
+     * 
+ * + * + * + * @param isSimple 是否简单模式,简单模式为不带'-'的UUID字符串 + * @return 此{@code UUID} 的字符串表现形式 + */ + public String toString(boolean isSimple) + { + final StringBuilder builder = new StringBuilder(isSimple ? 32 : 36); + // time_low + builder.append(digits(mostSigBits >> 32, 8)); + if (!isSimple) + { + builder.append('-'); + } + // time_mid + builder.append(digits(mostSigBits >> 16, 4)); + if (!isSimple) + { + builder.append('-'); + } + // time_high_and_version + builder.append(digits(mostSigBits, 4)); + if (!isSimple) + { + builder.append('-'); + } + // variant_and_sequence + builder.append(digits(leastSigBits >> 48, 4)); + if (!isSimple) + { + builder.append('-'); + } + // node + builder.append(digits(leastSigBits, 12)); + + return builder.toString(); + } + + /** + * 返回此 UUID 的哈希码。 + * + * @return UUID 的哈希码值。 + */ + @Override + public int hashCode() + { + long hilo = mostSigBits ^ leastSigBits; + return ((int) (hilo >> 32)) ^ (int) hilo; + } + + /** + * 将此对象与指定对象比较。 + *

+ * 当且仅当参数不为 {@code null}、而是一个 UUID 对象、具有与此 UUID 相同的 varriant、包含相同的值(每一位均相同)时,结果才为 {@code true}。 + * + * @param obj 要与之比较的对象 + * + * @return 如果对象相同,则返回 {@code true};否则返回 {@code false} + */ + @Override + public boolean equals(Object obj) + { + if ((null == obj) || (obj.getClass() != UUID.class)) + { + return false; + } + UUID id = (UUID) obj; + return (mostSigBits == id.mostSigBits && leastSigBits == id.leastSigBits); + } + + // Comparison Operations + + /** + * 将此 UUID 与指定的 UUID 比较。 + * + *

+ * 如果两个 UUID 不同,且第一个 UUID 的最高有效字段大于第二个 UUID 的对应字段,则第一个 UUID 大于第二个 UUID。 + * + * @param val 与此 UUID 比较的 UUID + * + * @return 在此 UUID 小于、等于或大于 val 时,分别返回 -1、0 或 1。 + * + */ + @Override + public int compareTo(UUID val) + { + // The ordering is intentionally set up so that the UUIDs + // can simply be numerically compared as two numbers + return (this.mostSigBits < val.mostSigBits ? -1 : // + (this.mostSigBits > val.mostSigBits ? 1 : // + (this.leastSigBits < val.leastSigBits ? -1 : // + (this.leastSigBits > val.leastSigBits ? 1 : // + 0)))); + } + + // ------------------------------------------------------------------------------------------------------------------- + // Private method start + /** + * 返回指定数字对应的hex值 + * + * @param val 值 + * @param digits 位 + * @return 值 + */ + private static String digits(long val, int digits) + { + long hi = 1L << (digits * 4); + return Long.toHexString(hi | (val & (hi - 1))).substring(1); + } + + /** + * 检查是否为time-based版本UUID + */ + private void checkTimeBase() + { + if (version() != 1) + { + throw new UnsupportedOperationException("Not a time-based UUID"); + } + } + + /** + * 获取{@link SecureRandom},类提供加密的强随机数生成器 (RNG) + * + * @return {@link SecureRandom} + */ + public static SecureRandom getSecureRandom() + { + try + { + return SecureRandom.getInstance("SHA1PRNG"); + } + catch (NoSuchAlgorithmException e) + { + throw new UtilException(e); + } + } + + /** + * 获取随机数生成器对象
+ * ThreadLocalRandom是JDK 7之后提供并发产生随机数,能够解决多个线程发生的竞争争夺。 + * + * @return {@link ThreadLocalRandom} + */ + public static ThreadLocalRandom getRandom() + { + return ThreadLocalRandom.current(); + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/xss/Xss.java b/ruoyi-common/src/main/java/com/ruoyi/common/xss/Xss.java new file mode 100644 index 0000000..7bfdf04 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/xss/Xss.java @@ -0,0 +1,27 @@ +package com.ruoyi.common.xss; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 自定义xss校验注解 + * + * @author ruoyi + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value = { ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER }) +@Constraint(validatedBy = { XssValidator.class }) +public @interface Xss +{ + String message() + + default "不允许任何脚本运行"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/xss/XssValidator.java b/ruoyi-common/src/main/java/com/ruoyi/common/xss/XssValidator.java new file mode 100644 index 0000000..42f425c --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/xss/XssValidator.java @@ -0,0 +1,39 @@ +package com.ruoyi.common.xss; + +import com.ruoyi.common.utils.StringUtils; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 自定义xss校验注解实现 + * + * @author ruoyi + */ +public class XssValidator implements ConstraintValidator +{ + private static final String HTML_PATTERN = "<(\\S*?)[^>]*>.*?|<.*? />"; + + @Override + public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) + { + if (StringUtils.isBlank(value)) + { + return true; + } + return !containsHtml(value); + } + + public static boolean containsHtml(String value) + { + StringBuilder sHtml = new StringBuilder(); + Pattern pattern = Pattern.compile(HTML_PATTERN); + Matcher matcher = pattern.matcher(value); + while (matcher.find()) + { + sHtml.append(matcher.group()); + } + return pattern.matcher(sHtml).matches(); + } +} \ No newline at end of file diff --git a/ruoyi-framework/pom.xml b/ruoyi-framework/pom.xml new file mode 100644 index 0000000..e42a12f --- /dev/null +++ b/ruoyi-framework/pom.xml @@ -0,0 +1,64 @@ + + + + ruoyi + com.ruoyi + 3.9.0 + + 4.0.0 + + ruoyi-framework + + + framework框架核心 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + com.alibaba + druid-spring-boot-starter + + + + + pro.fessional + kaptcha + + + servlet-api + javax.servlet + + + + + + + com.github.oshi + oshi-core + + + + + com.ruoyi + ruoyi-system + + + + + \ No newline at end of file diff --git a/ruoyi-framework/ruoyi-framework.iml b/ruoyi-framework/ruoyi-framework.iml new file mode 100644 index 0000000..4175059 --- /dev/null +++ b/ruoyi-framework/ruoyi-framework.iml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataScopeAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataScopeAspect.java new file mode 100644 index 0000000..b2337c9 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataScopeAspect.java @@ -0,0 +1,184 @@ +package com.ruoyi.framework.aspectj; + +import java.util.ArrayList; +import java.util.List; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.stereotype.Component; +import com.ruoyi.common.annotation.DataScope; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.security.context.PermissionContextHolder; + +/** + * 数据过滤处理 + * + * @author ruoyi + */ +@Aspect +@Component +public class DataScopeAspect +{ + /** + * 全部数据权限 + */ + public static final String DATA_SCOPE_ALL = "1"; + + /** + * 自定数据权限 + */ + public static final String DATA_SCOPE_CUSTOM = "2"; + + /** + * 部门数据权限 + */ + public static final String DATA_SCOPE_DEPT = "3"; + + /** + * 部门及以下数据权限 + */ + public static final String DATA_SCOPE_DEPT_AND_CHILD = "4"; + + /** + * 仅本人数据权限 + */ + public static final String DATA_SCOPE_SELF = "5"; + + /** + * 数据权限过滤关键字 + */ + public static final String DATA_SCOPE = "dataScope"; + + @Before("@annotation(controllerDataScope)") + public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable + { + clearDataScope(point); + handleDataScope(point, controllerDataScope); + } + + protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope) + { + // 获取当前的用户 + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNotNull(loginUser)) + { + SysUser currentUser = loginUser.getUser(); + // 如果是超级管理员,则不过滤数据 + if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin()) + { + String permission = StringUtils.defaultIfEmpty(controllerDataScope.permission(), PermissionContextHolder.getContext()); + dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(), controllerDataScope.userAlias(), permission); + } + } + } + + /** + * 数据范围过滤 + * + * @param joinPoint 切点 + * @param user 用户 + * @param deptAlias 部门别名 + * @param userAlias 用户别名 + * @param permission 权限字符 + */ + public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias, String permission) + { + StringBuilder sqlString = new StringBuilder(); + List conditions = new ArrayList(); + List scopeCustomIds = new ArrayList(); + user.getRoles().forEach(role -> { + if (DATA_SCOPE_CUSTOM.equals(role.getDataScope()) && StringUtils.equals(role.getStatus(), UserConstants.ROLE_NORMAL) && StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission))) + { + scopeCustomIds.add(Convert.toStr(role.getRoleId())); + } + }); + + for (SysRole role : user.getRoles()) + { + String dataScope = role.getDataScope(); + if (conditions.contains(dataScope) || StringUtils.equals(role.getStatus(), UserConstants.ROLE_DISABLE)) + { + continue; + } + if (!StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission))) + { + continue; + } + if (DATA_SCOPE_ALL.equals(dataScope)) + { + sqlString = new StringBuilder(); + conditions.add(dataScope); + break; + } + else if (DATA_SCOPE_CUSTOM.equals(dataScope)) + { + if (scopeCustomIds.size() > 1) + { + // 多个自定数据权限使用in查询,避免多次拼接。 + sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id in ({}) ) ", deptAlias, String.join(",", scopeCustomIds))); + } + else + { + sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias, role.getRoleId())); + } + } + else if (DATA_SCOPE_DEPT.equals(dataScope)) + { + sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId())); + } + else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)) + { + sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )", deptAlias, user.getDeptId(), user.getDeptId())); + } + else if (DATA_SCOPE_SELF.equals(dataScope)) + { + if (StringUtils.isNotBlank(userAlias)) + { + sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId())); + } + else + { + // 数据权限为仅本人且没有userAlias别名不查询任何数据 + sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias)); + } + } + conditions.add(dataScope); + } + + // 角色都不包含传递过来的权限字符,这个时候sqlString也会为空,所以要限制一下,不查询任何数据 + if (StringUtils.isEmpty(conditions)) + { + sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias)); + } + + if (StringUtils.isNotBlank(sqlString.toString())) + { + Object params = joinPoint.getArgs()[0]; + if (StringUtils.isNotNull(params) && params instanceof BaseEntity) + { + BaseEntity baseEntity = (BaseEntity) params; + baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")"); + } + } + } + + /** + * 拼接权限sql前先清空params.dataScope参数防止注入 + */ + private void clearDataScope(final JoinPoint joinPoint) + { + Object params = joinPoint.getArgs()[0]; + if (StringUtils.isNotNull(params) && params instanceof BaseEntity) + { + BaseEntity baseEntity = (BaseEntity) params; + baseEntity.getParams().put(DATA_SCOPE, ""); + } + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataSourceAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataSourceAspect.java new file mode 100644 index 0000000..8c2c9f4 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataSourceAspect.java @@ -0,0 +1,72 @@ +package com.ruoyi.framework.aspectj; + +import java.util.Objects; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import com.ruoyi.common.annotation.DataSource; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.datasource.DynamicDataSourceContextHolder; + +/** + * 多数据源处理 + * + * @author ruoyi + */ +@Aspect +@Order(1) +@Component +public class DataSourceAspect +{ + protected Logger logger = LoggerFactory.getLogger(getClass()); + + @Pointcut("@annotation(com.ruoyi.common.annotation.DataSource)" + + "|| @within(com.ruoyi.common.annotation.DataSource)") + public void dsPointCut() + { + + } + + @Around("dsPointCut()") + public Object around(ProceedingJoinPoint point) throws Throwable + { + DataSource dataSource = getDataSource(point); + + if (StringUtils.isNotNull(dataSource)) + { + DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name()); + } + + try + { + return point.proceed(); + } + finally + { + // 销毁数据源 在执行方法之后 + DynamicDataSourceContextHolder.clearDataSourceType(); + } + } + + /** + * 获取需要切换的数据源 + */ + public DataSource getDataSource(ProceedingJoinPoint point) + { + MethodSignature signature = (MethodSignature) point.getSignature(); + DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class); + if (Objects.nonNull(dataSource)) + { + return dataSource; + } + + return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java new file mode 100644 index 0000000..bacf820 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java @@ -0,0 +1,256 @@ +package com.ruoyi.framework.aspectj; + +import java.util.Collection; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.ArrayUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.NamedThreadLocal; +import org.springframework.stereotype.Component; +import org.springframework.validation.BindingResult; +import org.springframework.web.multipart.MultipartFile; +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.enums.BusinessStatus; +import com.ruoyi.common.enums.HttpMethod; +import com.ruoyi.common.filter.PropertyPreExcludeFilter; +import com.ruoyi.common.utils.ExceptionUtil; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.system.domain.SysOperLog; + +/** + * 操作日志记录处理 + * + * @author ruoyi + */ +@Aspect +@Component +public class LogAspect +{ + private static final Logger log = LoggerFactory.getLogger(LogAspect.class); + + /** 排除敏感属性字段 */ + public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" }; + + /** 计算操作消耗时间 */ + private static final ThreadLocal TIME_THREADLOCAL = new NamedThreadLocal("Cost Time"); + + /** + * 处理请求前执行 + */ + @Before(value = "@annotation(controllerLog)") + public void doBefore(JoinPoint joinPoint, Log controllerLog) + { + TIME_THREADLOCAL.set(System.currentTimeMillis()); + } + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) + { + handleLog(joinPoint, controllerLog, null, jsonResult); + } + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) + { + handleLog(joinPoint, controllerLog, e, null); + } + + protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) + { + try + { + // 获取当前的用户 + LoginUser loginUser = SecurityUtils.getLoginUser(); + + // *========数据库日志=========*// + SysOperLog operLog = new SysOperLog(); + operLog.setStatus(BusinessStatus.SUCCESS.ordinal()); + // 请求的地址 + String ip = IpUtils.getIpAddr(); + operLog.setOperIp(ip); + operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255)); + if (loginUser != null) + { + operLog.setOperName(loginUser.getUsername()); + SysUser currentUser = loginUser.getUser(); + if (StringUtils.isNotNull(currentUser) && StringUtils.isNotNull(currentUser.getDept())) + { + operLog.setDeptName(currentUser.getDept().getDeptName()); + } + } + + if (e != null) + { + operLog.setStatus(BusinessStatus.FAIL.ordinal()); + operLog.setErrorMsg(StringUtils.substring(Convert.toStr(e.getMessage(), ExceptionUtil.getExceptionMessage(e)), 0, 2000)); + } + // 设置方法名称 + String className = joinPoint.getTarget().getClass().getName(); + String methodName = joinPoint.getSignature().getName(); + operLog.setMethod(className + "." + methodName + "()"); + // 设置请求方式 + operLog.setRequestMethod(ServletUtils.getRequest().getMethod()); + // 处理设置注解上的参数 + getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult); + // 设置消耗时间 + operLog.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get()); + // 保存数据库 + AsyncManager.me().execute(AsyncFactory.recordOper(operLog)); + } + catch (Exception exp) + { + // 记录本地异常日志 + log.error("异常信息:{}", exp.getMessage()); + exp.printStackTrace(); + } + finally + { + TIME_THREADLOCAL.remove(); + } + } + + /** + * 获取注解中对方法的描述信息 用于Controller层注解 + * + * @param log 日志 + * @param operLog 操作日志 + * @throws Exception + */ + public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception + { + // 设置action动作 + operLog.setBusinessType(log.businessType().ordinal()); + // 设置标题 + operLog.setTitle(log.title()); + // 设置操作人类别 + operLog.setOperatorType(log.operatorType().ordinal()); + // 是否需要保存request,参数和值 + if (log.isSaveRequestData()) + { + // 获取参数的信息,传入到数据库中。 + setRequestValue(joinPoint, operLog, log.excludeParamNames()); + } + // 是否需要保存response,参数和值 + if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult)) + { + operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000)); + } + } + + /** + * 获取请求的参数,放到log中 + * + * @param operLog 操作日志 + * @throws Exception 异常 + */ + private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog, String[] excludeParamNames) throws Exception + { + Map paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest()); + String requestMethod = operLog.getRequestMethod(); + if (StringUtils.isEmpty(paramsMap) && StringUtils.equalsAny(requestMethod, HttpMethod.PUT.name(), HttpMethod.POST.name(), HttpMethod.DELETE.name())) + { + String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames); + operLog.setOperParam(StringUtils.substring(params, 0, 2000)); + } + else + { + operLog.setOperParam(StringUtils.substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter(excludeParamNames)), 0, 2000)); + } + } + + /** + * 参数拼装 + */ + private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) + { + String params = ""; + if (paramsArray != null && paramsArray.length > 0) + { + for (Object o : paramsArray) + { + if (StringUtils.isNotNull(o) && !isFilterObject(o)) + { + try + { + String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter(excludeParamNames)); + params += jsonObj.toString() + " "; + } + catch (Exception e) + { + } + } + } + } + return params.trim(); + } + + /** + * 忽略敏感属性 + */ + public PropertyPreExcludeFilter excludePropertyPreFilter(String[] excludeParamNames) + { + return new PropertyPreExcludeFilter().addExcludes(ArrayUtils.addAll(EXCLUDE_PROPERTIES, excludeParamNames)); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param o 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + @SuppressWarnings("rawtypes") + public boolean isFilterObject(final Object o) + { + Class clazz = o.getClass(); + if (clazz.isArray()) + { + return clazz.getComponentType().isAssignableFrom(MultipartFile.class); + } + else if (Collection.class.isAssignableFrom(clazz)) + { + Collection collection = (Collection) o; + for (Object value : collection) + { + return value instanceof MultipartFile; + } + } + else if (Map.class.isAssignableFrom(clazz)) + { + Map map = (Map) o; + for (Object value : map.entrySet()) + { + Map.Entry entry = (Map.Entry) value; + return entry.getValue() instanceof MultipartFile; + } + } + return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse + || o instanceof BindingResult; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java new file mode 100644 index 0000000..b720bc1 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java @@ -0,0 +1,89 @@ +package com.ruoyi.framework.aspectj; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.stereotype.Component; +import com.ruoyi.common.annotation.RateLimiter; +import com.ruoyi.common.enums.LimitType; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.IpUtils; + +/** + * 限流处理 + * + * @author ruoyi + */ +@Aspect +@Component +public class RateLimiterAspect +{ + private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class); + + private RedisTemplate redisTemplate; + + private RedisScript limitScript; + + @Autowired + public void setRedisTemplate1(RedisTemplate redisTemplate) + { + this.redisTemplate = redisTemplate; + } + + @Autowired + public void setLimitScript(RedisScript limitScript) + { + this.limitScript = limitScript; + } + + @Before("@annotation(rateLimiter)") + public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable + { + int time = rateLimiter.time(); + int count = rateLimiter.count(); + + String combineKey = getCombineKey(rateLimiter, point); + List keys = Collections.singletonList(combineKey); + try + { + Long number = redisTemplate.execute(limitScript, keys, count, time); + if (StringUtils.isNull(number) || number.intValue() > count) + { + throw new ServiceException("访问过于频繁,请稍候再试"); + } + log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), combineKey); + } + catch (ServiceException e) + { + throw e; + } + catch (Exception e) + { + throw new RuntimeException("服务器限流异常,请稍候再试"); + } + } + + public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) + { + StringBuffer stringBuffer = new StringBuffer(rateLimiter.key()); + if (rateLimiter.limitType() == LimitType.IP) + { + stringBuffer.append(IpUtils.getIpAddr()).append("-"); + } + MethodSignature signature = (MethodSignature) point.getSignature(); + Method method = signature.getMethod(); + Class targetClass = method.getDeclaringClass(); + stringBuffer.append(targetClass.getName()).append("-").append(method.getName()); + return stringBuffer.toString(); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ApplicationConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ApplicationConfig.java new file mode 100644 index 0000000..1d4dc1f --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ApplicationConfig.java @@ -0,0 +1,30 @@ +package com.ruoyi.framework.config; + +import java.util.TimeZone; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +/** + * 程序注解配置 + * + * @author ruoyi + */ +@Configuration +// 表示通过aop框架暴露该代理对象,AopContext能够访问 +@EnableAspectJAutoProxy(exposeProxy = true) +// 指定要扫描的Mapper类的包的路径 +@MapperScan("com.ruoyi.**.mapper") +public class ApplicationConfig +{ + /** + * 时区配置 + */ + @Bean + public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization() + { + return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault()); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/CaptchaConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/CaptchaConfig.java new file mode 100644 index 0000000..43e78ae --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/CaptchaConfig.java @@ -0,0 +1,83 @@ +package com.ruoyi.framework.config; + +import java.util.Properties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import com.google.code.kaptcha.impl.DefaultKaptcha; +import com.google.code.kaptcha.util.Config; +import static com.google.code.kaptcha.Constants.*; + +/** + * 验证码配置 + * + * @author ruoyi + */ +@Configuration +public class CaptchaConfig +{ + @Bean(name = "captchaProducer") + public DefaultKaptcha getKaptchaBean() + { + DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + // 是否有边框 默认为true 我们可以自己设置yes,no + properties.setProperty(KAPTCHA_BORDER, "yes"); + // 验证码文本字符颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black"); + // 验证码图片宽度 默认为200 + properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160"); + // 验证码图片高度 默认为50 + properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60"); + // 验证码文本字符大小 默认为40 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38"); + // KAPTCHA_SESSION_KEY + properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode"); + // 验证码文本字符长度 默认为5 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4"); + // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize) + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); + // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy + properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); + Config config = new Config(properties); + defaultKaptcha.setConfig(config); + return defaultKaptcha; + } + + @Bean(name = "captchaProducerMath") + public DefaultKaptcha getKaptchaBeanMath() + { + DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + // 是否有边框 默认为true 我们可以自己设置yes,no + properties.setProperty(KAPTCHA_BORDER, "yes"); + // 边框颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90"); + // 验证码文本字符颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue"); + // 验证码图片宽度 默认为200 + properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160"); + // 验证码图片高度 默认为50 + properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60"); + // 验证码文本字符大小 默认为40 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35"); + // KAPTCHA_SESSION_KEY + properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath"); + // 验证码文本生成器 + properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.ruoyi.framework.config.KaptchaTextCreator"); + // 验证码文本字符间距 默认为2 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3"); + // 验证码文本字符长度 默认为5 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6"); + // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize) + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); + // 验证码噪点颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_NOISE_COLOR, "white"); + // 干扰实现类 + properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise"); + // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy + properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); + Config config = new Config(properties); + defaultKaptcha.setConfig(config); + return defaultKaptcha; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/DruidConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/DruidConfig.java new file mode 100644 index 0000000..f6abac1 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/DruidConfig.java @@ -0,0 +1,126 @@ +package com.ruoyi.framework.config; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.sql.DataSource; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; +import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties; +import com.alibaba.druid.util.Utils; +import com.ruoyi.common.enums.DataSourceType; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.framework.config.properties.DruidProperties; +import com.ruoyi.framework.datasource.DynamicDataSource; + +/** + * druid 配置多数据源 + * + * @author ruoyi + */ +@Configuration +public class DruidConfig +{ + @Bean + @ConfigurationProperties("spring.datasource.druid.master") + public DataSource masterDataSource(DruidProperties druidProperties) + { + DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); + return druidProperties.dataSource(dataSource); + } + + @Bean + @ConfigurationProperties("spring.datasource.druid.slave") + @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true") + public DataSource slaveDataSource(DruidProperties druidProperties) + { + DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); + return druidProperties.dataSource(dataSource); + } + + @Bean(name = "dynamicDataSource") + @Primary + public DynamicDataSource dataSource(DataSource masterDataSource) + { + Map targetDataSources = new HashMap<>(); + targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource); + setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource"); + return new DynamicDataSource(masterDataSource, targetDataSources); + } + + /** + * 设置数据源 + * + * @param targetDataSources 备选数据源集合 + * @param sourceName 数据源名称 + * @param beanName bean名称 + */ + public void setDataSource(Map targetDataSources, String sourceName, String beanName) + { + try + { + DataSource dataSource = SpringUtils.getBean(beanName); + targetDataSources.put(sourceName, dataSource); + } + catch (Exception e) + { + } + } + + /** + * 去除监控页面底部的广告 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Bean + @ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true") + public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties) + { + // 获取web监控页面的参数 + DruidStatProperties.StatViewServlet config = properties.getStatViewServlet(); + // 提取common.js的配置路径 + String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*"; + String commonJsPattern = pattern.replaceAll("\\*", "js/common.js"); + final String filePath = "support/http/resources/js/common.js"; + // 创建filter进行过滤 + Filter filter = new Filter() + { + @Override + public void init(javax.servlet.FilterConfig filterConfig) throws ServletException + { + } + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException + { + chain.doFilter(request, response); + // 重置缓冲区,响应头不会被重置 + response.resetBuffer(); + // 获取common.js + String text = Utils.readFromResource(filePath); + // 正则替换banner, 除去底部的广告信息 + text = text.replaceAll("
", ""); + text = text.replaceAll("powered.*?shrek.wang", ""); + response.getWriter().write(text); + } + @Override + public void destroy() + { + } + }; + FilterRegistrationBean registrationBean = new FilterRegistrationBean(); + registrationBean.setFilter(filter); + registrationBean.addUrlPatterns(commonJsPattern); + return registrationBean; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java new file mode 100644 index 0000000..4adbb7f --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java @@ -0,0 +1,52 @@ +package com.ruoyi.framework.config; + +import java.nio.charset.Charset; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.filter.Filter; +import com.ruoyi.common.constant.Constants; + +/** + * Redis使用FastJson序列化 + * + * @author ruoyi + */ +public class FastJson2JsonRedisSerializer implements RedisSerializer +{ + public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR); + + private Class clazz; + + public FastJson2JsonRedisSerializer(Class clazz) + { + super(); + this.clazz = clazz; + } + + @Override + public byte[] serialize(T t) throws SerializationException + { + if (t == null) + { + return new byte[0]; + } + return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET); + } + + @Override + public T deserialize(byte[] bytes) throws SerializationException + { + if (bytes == null || bytes.length <= 0) + { + return null; + } + String str = new String(bytes, DEFAULT_CHARSET); + + return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java new file mode 100644 index 0000000..bb14c04 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java @@ -0,0 +1,58 @@ +package com.ruoyi.framework.config; + +import java.util.HashMap; +import java.util.Map; +import javax.servlet.DispatcherType; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import com.ruoyi.common.filter.RepeatableFilter; +import com.ruoyi.common.filter.XssFilter; +import com.ruoyi.common.utils.StringUtils; + +/** + * Filter配置 + * + * @author ruoyi + */ +@Configuration +public class FilterConfig +{ + @Value("${xss.excludes}") + private String excludes; + + @Value("${xss.urlPatterns}") + private String urlPatterns; + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Bean + @ConditionalOnProperty(value = "xss.enabled", havingValue = "true") + public FilterRegistrationBean xssFilterRegistration() + { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setDispatcherTypes(DispatcherType.REQUEST); + registration.setFilter(new XssFilter()); + registration.addUrlPatterns(StringUtils.split(urlPatterns, ",")); + registration.setName("xssFilter"); + registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE); + Map initParameters = new HashMap(); + initParameters.put("excludes", excludes); + registration.setInitParameters(initParameters); + return registration; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Bean + public FilterRegistrationBean someFilterRegistration() + { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setFilter(new RepeatableFilter()); + registration.addUrlPatterns("/*"); + registration.setName("repeatableFilter"); + registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE); + return registration; + } + +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.java new file mode 100644 index 0000000..163fd01 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.java @@ -0,0 +1,43 @@ +package com.ruoyi.framework.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; +import org.springframework.web.servlet.i18n.SessionLocaleResolver; +import com.ruoyi.common.constant.Constants; + +/** + * 资源文件配置加载 + * + * @author ruoyi + */ +@Configuration +public class I18nConfig implements WebMvcConfigurer +{ + @Bean + public LocaleResolver localeResolver() + { + SessionLocaleResolver slr = new SessionLocaleResolver(); + // 默认语言 + slr.setDefaultLocale(Constants.DEFAULT_LOCALE); + return slr; + } + + @Bean + public LocaleChangeInterceptor localeChangeInterceptor() + { + LocaleChangeInterceptor lci = new LocaleChangeInterceptor(); + // 参数名 + lci.setParamName("lang"); + return lci; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) + { + registry.addInterceptor(localeChangeInterceptor()); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/KaptchaTextCreator.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/KaptchaTextCreator.java new file mode 100644 index 0000000..7f8e1d5 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/KaptchaTextCreator.java @@ -0,0 +1,68 @@ +package com.ruoyi.framework.config; + +import java.util.Random; +import com.google.code.kaptcha.text.impl.DefaultTextCreator; + +/** + * 验证码文本生成器 + * + * @author ruoyi + */ +public class KaptchaTextCreator extends DefaultTextCreator +{ + private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(","); + + @Override + public String getText() + { + Integer result = 0; + Random random = new Random(); + int x = random.nextInt(10); + int y = random.nextInt(10); + StringBuilder suChinese = new StringBuilder(); + int randomoperands = random.nextInt(3); + if (randomoperands == 0) + { + result = x * y; + suChinese.append(CNUMBERS[x]); + suChinese.append("*"); + suChinese.append(CNUMBERS[y]); + } + else if (randomoperands == 1) + { + if ((x != 0) && y % x == 0) + { + result = y / x; + suChinese.append(CNUMBERS[y]); + suChinese.append("/"); + suChinese.append(CNUMBERS[x]); + } + else + { + result = x + y; + suChinese.append(CNUMBERS[x]); + suChinese.append("+"); + suChinese.append(CNUMBERS[y]); + } + } + else + { + if (x >= y) + { + result = x - y; + suChinese.append(CNUMBERS[x]); + suChinese.append("-"); + suChinese.append(CNUMBERS[y]); + } + else + { + result = y - x; + suChinese.append(CNUMBERS[y]); + suChinese.append("-"); + suChinese.append(CNUMBERS[x]); + } + } + suChinese.append("=?@" + result); + return suChinese.toString(); + } +} \ No newline at end of file diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java new file mode 100644 index 0000000..057c941 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java @@ -0,0 +1,132 @@ +package com.ruoyi.framework.config; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import javax.sql.DataSource; +import org.apache.ibatis.io.VFS; +import org.apache.ibatis.session.SqlSessionFactory; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.mybatis.spring.boot.autoconfigure.SpringBootVFS; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.util.ClassUtils; +import com.ruoyi.common.utils.StringUtils; + +/** + * Mybatis支持*匹配扫描包 + * + * @author ruoyi + */ +@Configuration +public class MyBatisConfig +{ + @Autowired + private Environment env; + + static final String DEFAULT_RESOURCE_PATTERN = "**/*.class"; + + public static String setTypeAliasesPackage(String typeAliasesPackage) + { + ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver(); + MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver); + List allResult = new ArrayList(); + try + { + for (String aliasesPackage : typeAliasesPackage.split(",")) + { + List result = new ArrayList(); + aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN; + Resource[] resources = resolver.getResources(aliasesPackage); + if (resources != null && resources.length > 0) + { + MetadataReader metadataReader = null; + for (Resource resource : resources) + { + if (resource.isReadable()) + { + metadataReader = metadataReaderFactory.getMetadataReader(resource); + try + { + result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName()); + } + catch (ClassNotFoundException e) + { + e.printStackTrace(); + } + } + } + } + if (result.size() > 0) + { + HashSet hashResult = new HashSet(result); + allResult.addAll(hashResult); + } + } + if (allResult.size() > 0) + { + typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0])); + } + else + { + throw new RuntimeException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包"); + } + } + catch (IOException e) + { + e.printStackTrace(); + } + return typeAliasesPackage; + } + + public Resource[] resolveMapperLocations(String[] mapperLocations) + { + ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver(); + List resources = new ArrayList(); + if (mapperLocations != null) + { + for (String mapperLocation : mapperLocations) + { + try + { + Resource[] mappers = resourceResolver.getResources(mapperLocation); + resources.addAll(Arrays.asList(mappers)); + } + catch (IOException e) + { + // ignore + } + } + } + return resources.toArray(new Resource[resources.size()]); + } + + @Bean + public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception + { + String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage"); + String mapperLocations = env.getProperty("mybatis.mapperLocations"); + String configLocation = env.getProperty("mybatis.configLocation"); + typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage); + VFS.addImplClass(SpringBootVFS.class); + + final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); + sessionFactory.setDataSource(dataSource); + sessionFactory.setTypeAliasesPackage(typeAliasesPackage); + sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ","))); + sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation)); + return sessionFactory.getObject(); + } +} \ No newline at end of file diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java new file mode 100644 index 0000000..3f4f485 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java @@ -0,0 +1,69 @@ +package com.ruoyi.framework.config; + +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * redis配置 + * + * @author ruoyi + */ +@Configuration +@EnableCaching +public class RedisConfig extends CachingConfigurerSupport +{ + @Bean + @SuppressWarnings(value = { "unchecked", "rawtypes" }) + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) + { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class); + + // 使用StringRedisSerializer来序列化和反序列化redis的key值 + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(serializer); + + // Hash的key也采用StringRedisSerializer的序列化方式 + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(serializer); + + template.afterPropertiesSet(); + return template; + } + + @Bean + public DefaultRedisScript limitScript() + { + DefaultRedisScript redisScript = new DefaultRedisScript<>(); + redisScript.setScriptText(limitScriptText()); + redisScript.setResultType(Long.class); + return redisScript; + } + + /** + * 限流脚本 + */ + private String limitScriptText() + { + return "local key = KEYS[1]\n" + + "local count = tonumber(ARGV[1])\n" + + "local time = tonumber(ARGV[2])\n" + + "local current = redis.call('get', key);\n" + + "if current and tonumber(current) > count then\n" + + " return tonumber(current);\n" + + "end\n" + + "current = redis.call('incr', key)\n" + + "if tonumber(current) == 1 then\n" + + " redis.call('expire', key, time)\n" + + "end\n" + + "return tonumber(current);"; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java new file mode 100644 index 0000000..0f48b11 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java @@ -0,0 +1,72 @@ +package com.ruoyi.framework.config; + +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; +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.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor; + +/** + * 通用配置 + * + * @author ruoyi + */ +@Configuration +public class ResourcesConfig implements WebMvcConfigurer +{ + @Autowired + private RepeatSubmitInterceptor repeatSubmitInterceptor; + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) + { + /** 本地文件上传路径 */ + registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**") + .addResourceLocations("file:" + RuoYiConfig.getProfile() + "/"); + + /** swagger配置 */ + registry.addResourceHandler("/swagger-ui/**") + .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/") + .setCacheControl(CacheControl.maxAge(5, TimeUnit.HOURS).cachePublic()); + } + + /** + * 自定义拦截规则 + */ + @Override + public void addInterceptors(InterceptorRegistry registry) + { + registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); + } + + /** + * 跨域配置 + */ + @Bean + public CorsFilter corsFilter() + { + CorsConfiguration config = new CorsConfiguration(); + // 设置访问源地址 + config.addAllowedOriginPattern("*"); + // 设置访问源请求头 + config.addAllowedHeader("*"); + // 设置访问源请求方法 + config.addAllowedMethod("*"); + // 有效期 1800秒 + config.setMaxAge(1800L); + // 添加映射路径,拦截一切请求 + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + // 返回新的CorsFilter + return new CorsFilter(source); + } +} \ No newline at end of file diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java new file mode 100644 index 0000000..f2cedb0 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java @@ -0,0 +1,142 @@ +package com.ruoyi.framework.config; + +import org.springframework.beans.factory.annotation.Autowired; +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.authentication.ProviderManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.web.filter.CorsFilter; +import com.ruoyi.framework.config.properties.PermitAllUrlProperties; +import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter; +import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl; +import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl; + +/** + * spring security配置 + * + * @author ruoyi + */ +@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true) +@Configuration +public class SecurityConfig +{ + /** + * 自定义用户认证逻辑 + */ + @Autowired + private UserDetailsService userDetailsService; + + /** + * 认证失败处理类 + */ + @Autowired + private AuthenticationEntryPointImpl unauthorizedHandler; + + /** + * 退出处理类 + */ + @Autowired + private LogoutSuccessHandlerImpl logoutSuccessHandler; + + /** + * token认证过滤器 + */ + @Autowired + private JwtAuthenticationTokenFilter authenticationTokenFilter; + + /** + * 跨域过滤器 + */ + @Autowired + private CorsFilter corsFilter; + + /** + * 允许匿名访问的地址 + */ + @Autowired + private PermitAllUrlProperties permitAllUrl; + + /** + * 身份验证实现 + */ + @Bean + public AuthenticationManager authenticationManager() + { + DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); + daoAuthenticationProvider.setUserDetailsService(userDetailsService); + daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder()); + return new ProviderManager(daoAuthenticationProvider); + } + + /** + * anyRequest | 匹配所有请求路径 + * access | SpringEl表达式结果为true时可以访问 + * anonymous | 匿名可以访问 + * denyAll | 用户不能访问 + * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) + * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 + * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问 + * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 + * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 + * hasRole | 如果有参数,参数表示角色,则其角色可以访问 + * permitAll | 用户可以任意访问 + * rememberMe | 允许通过remember-me登录的用户访问 + * authenticated | 用户登录后可访问 + */ + @Bean + protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception + { + return httpSecurity + // CSRF禁用,因为不使用session + .csrf(csrf -> csrf.disable()) + // 禁用HTTP响应标头 + .headers((headersCustomizer) -> { + headersCustomizer.cacheControl(cache -> cache.disable()).frameOptions(options -> options.sameOrigin()); + }) + // 认证失败处理类 + .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) + // 基于token,所以不需要session + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + // 注解标记允许匿名访问的url + .authorizeHttpRequests((requests) -> { + permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll()); + // 对于登录login 注册register 验证码captchaImage 允许匿名访问 + requests.antMatchers("/login", "/register", "/captchaImage").permitAll() + // 静态资源,可匿名访问 + .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() + .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll() + // open 统一放行 + .antMatchers("/open/**").permitAll() + + // 除上面外的所有请求全部需要鉴权认证 + .anyRequest().authenticated(); + }) + // 添加Logout filter + .logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler)) + // 添加JWT filter + .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class) + // 添加CORS filter + .addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class) + .addFilterBefore(corsFilter, LogoutFilter.class) + .build(); + } + + /** + * 强散列哈希加密实现 + */ + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() + { + return new BCryptPasswordEncoder(); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ServerConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ServerConfig.java new file mode 100644 index 0000000..b5b7de3 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ServerConfig.java @@ -0,0 +1,32 @@ +package com.ruoyi.framework.config; + +import javax.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Component; +import com.ruoyi.common.utils.ServletUtils; + +/** + * 服务相关配置 + * + * @author ruoyi + */ +@Component +public class ServerConfig +{ + /** + * 获取完整的请求路径,包括:域名,端口,上下文访问路径 + * + * @return 服务地址 + */ + public String getUrl() + { + HttpServletRequest request = ServletUtils.getRequest(); + return getDomain(request); + } + + public static String getDomain(HttpServletRequest request) + { + StringBuffer url = request.getRequestURL(); + String contextPath = request.getServletContext().getContextPath(); + return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString(); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java new file mode 100644 index 0000000..7840141 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java @@ -0,0 +1,63 @@ +package com.ruoyi.framework.config; + +import com.ruoyi.common.utils.Threads; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 线程池配置 + * + * @author ruoyi + **/ +@Configuration +public class ThreadPoolConfig +{ + // 核心线程池大小 + private int corePoolSize = 50; + + // 最大可创建的线程数 + private int maxPoolSize = 200; + + // 队列最大长度 + private int queueCapacity = 1000; + + // 线程池维护线程所允许的空闲时间 + private int keepAliveSeconds = 300; + + @Bean(name = "threadPoolTaskExecutor") + public ThreadPoolTaskExecutor threadPoolTaskExecutor() + { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setMaxPoolSize(maxPoolSize); + executor.setCorePoolSize(corePoolSize); + executor.setQueueCapacity(queueCapacity); + executor.setKeepAliveSeconds(keepAliveSeconds); + // 线程池对拒绝任务(无线程可用)的处理策略 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + return executor; + } + + /** + * 执行周期性或定时任务 + */ + @Bean(name = "scheduledExecutorService") + protected ScheduledExecutorService scheduledExecutorService() + { + return new ScheduledThreadPoolExecutor(corePoolSize, + new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(), + new ThreadPoolExecutor.CallerRunsPolicy()) + { + @Override + protected void afterExecute(Runnable r, Throwable t) + { + super.afterExecute(r, t); + Threads.printException(r, t); + } + }; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/DruidProperties.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/DruidProperties.java new file mode 100644 index 0000000..c8a5c8a --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/DruidProperties.java @@ -0,0 +1,89 @@ +package com.ruoyi.framework.config.properties; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import com.alibaba.druid.pool.DruidDataSource; + +/** + * druid 配置属性 + * + * @author ruoyi + */ +@Configuration +public class DruidProperties +{ + @Value("${spring.datasource.druid.initialSize}") + private int initialSize; + + @Value("${spring.datasource.druid.minIdle}") + private int minIdle; + + @Value("${spring.datasource.druid.maxActive}") + private int maxActive; + + @Value("${spring.datasource.druid.maxWait}") + private int maxWait; + + @Value("${spring.datasource.druid.connectTimeout}") + private int connectTimeout; + + @Value("${spring.datasource.druid.socketTimeout}") + private int socketTimeout; + + @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}") + private int timeBetweenEvictionRunsMillis; + + @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}") + private int minEvictableIdleTimeMillis; + + @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}") + private int maxEvictableIdleTimeMillis; + + @Value("${spring.datasource.druid.validationQuery}") + private String validationQuery; + + @Value("${spring.datasource.druid.testWhileIdle}") + private boolean testWhileIdle; + + @Value("${spring.datasource.druid.testOnBorrow}") + private boolean testOnBorrow; + + @Value("${spring.datasource.druid.testOnReturn}") + private boolean testOnReturn; + + public DruidDataSource dataSource(DruidDataSource datasource) + { + /** 配置初始化大小、最小、最大 */ + datasource.setInitialSize(initialSize); + datasource.setMaxActive(maxActive); + datasource.setMinIdle(minIdle); + + /** 配置获取连接等待超时的时间 */ + datasource.setMaxWait(maxWait); + + /** 配置驱动连接超时时间,检测数据库建立连接的超时时间,单位是毫秒 */ + datasource.setConnectTimeout(connectTimeout); + + /** 配置网络超时时间,等待数据库操作完成的网络超时时间,单位是毫秒 */ + datasource.setSocketTimeout(socketTimeout); + + /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */ + datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); + + /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */ + datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); + datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis); + + /** + * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 + */ + datasource.setValidationQuery(validationQuery); + /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */ + datasource.setTestWhileIdle(testWhileIdle); + /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ + datasource.setTestOnBorrow(testOnBorrow); + /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ + datasource.setTestOnReturn(testOnReturn); + return datasource; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/PermitAllUrlProperties.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/PermitAllUrlProperties.java new file mode 100644 index 0000000..29118fa --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/PermitAllUrlProperties.java @@ -0,0 +1,73 @@ +package com.ruoyi.framework.config.properties; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Pattern; +import org.apache.commons.lang3.RegExUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import com.ruoyi.common.annotation.Anonymous; + +/** + * 设置Anonymous注解允许匿名访问的url + * + * @author ruoyi + */ +@Configuration +public class PermitAllUrlProperties implements InitializingBean, ApplicationContextAware +{ + private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}"); + + private ApplicationContext applicationContext; + + private List urls = new ArrayList<>(); + + public String ASTERISK = "*"; + + @Override + public void afterPropertiesSet() + { + RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class); + Map map = mapping.getHandlerMethods(); + + map.keySet().forEach(info -> { + HandlerMethod handlerMethod = map.get(info); + + // 获取方法上边的注解 替代path variable 为 * + Anonymous method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Anonymous.class); + Optional.ofNullable(method).ifPresent(anonymous -> Objects.requireNonNull(info.getPatternsCondition().getPatterns()) + .forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK)))); + + // 获取类上边的注解, 替代path variable 为 * + Anonymous controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Anonymous.class); + Optional.ofNullable(controller).ifPresent(anonymous -> Objects.requireNonNull(info.getPatternsCondition().getPatterns()) + .forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK)))); + }); + } + + @Override + public void setApplicationContext(ApplicationContext context) throws BeansException + { + this.applicationContext = context; + } + + public List getUrls() + { + return urls; + } + + public void setUrls(List urls) + { + this.urls = urls; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSource.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSource.java new file mode 100644 index 0000000..e70b8cf --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSource.java @@ -0,0 +1,26 @@ +package com.ruoyi.framework.datasource; + +import java.util.Map; +import javax.sql.DataSource; +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; + +/** + * 动态数据源 + * + * @author ruoyi + */ +public class DynamicDataSource extends AbstractRoutingDataSource +{ + public DynamicDataSource(DataSource defaultTargetDataSource, Map targetDataSources) + { + super.setDefaultTargetDataSource(defaultTargetDataSource); + super.setTargetDataSources(targetDataSources); + super.afterPropertiesSet(); + } + + @Override + protected Object determineCurrentLookupKey() + { + return DynamicDataSourceContextHolder.getDataSourceType(); + } +} \ No newline at end of file diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSourceContextHolder.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSourceContextHolder.java new file mode 100644 index 0000000..9770af6 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSourceContextHolder.java @@ -0,0 +1,45 @@ +package com.ruoyi.framework.datasource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 数据源切换处理 + * + * @author ruoyi + */ +public class DynamicDataSourceContextHolder +{ + public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class); + + /** + * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本, + * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。 + */ + private static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal<>(); + + /** + * 设置数据源的变量 + */ + public static void setDataSourceType(String dsType) + { + log.info("切换到{}数据源", dsType); + CONTEXT_HOLDER.set(dsType); + } + + /** + * 获得数据源的变量 + */ + public static String getDataSourceType() + { + return CONTEXT_HOLDER.get(); + } + + /** + * 清空数据源变量 + */ + public static void clearDataSourceType() + { + CONTEXT_HOLDER.remove(); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.java new file mode 100644 index 0000000..c49eaf4 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.java @@ -0,0 +1,56 @@ +package com.ruoyi.framework.interceptor; + +import java.lang.reflect.Method; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.annotation.RepeatSubmit; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.ServletUtils; + +/** + * 防止重复提交拦截器 + * + * @author ruoyi + */ +@Component +public abstract class RepeatSubmitInterceptor implements HandlerInterceptor +{ + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception + { + if (handler instanceof HandlerMethod) + { + HandlerMethod handlerMethod = (HandlerMethod) handler; + Method method = handlerMethod.getMethod(); + RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class); + if (annotation != null) + { + if (this.isRepeatSubmit(request, annotation)) + { + AjaxResult ajaxResult = AjaxResult.error(annotation.message()); + ServletUtils.renderString(response, JSON.toJSONString(ajaxResult)); + return false; + } + } + return true; + } + else + { + return true; + } + } + + /** + * 验证是否重复提交由子类实现具体的防重复提交的规则 + * + * @param request 请求信息 + * @param annotation 防重复注解参数 + * @return 结果 + * @throws Exception + */ + public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation); +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java new file mode 100644 index 0000000..9dc9511 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java @@ -0,0 +1,110 @@ +package com.ruoyi.framework.interceptor.impl; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.annotation.RepeatSubmit; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.filter.RepeatedlyRequestWrapper; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.http.HttpHelper; +import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor; + +/** + * 判断请求url和数据是否和上一次相同, + * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。 + * + * @author ruoyi + */ +@Component +public class SameUrlDataInterceptor extends RepeatSubmitInterceptor +{ + public final String REPEAT_PARAMS = "repeatParams"; + + public final String REPEAT_TIME = "repeatTime"; + + // 令牌自定义标识 + @Value("${token.header}") + private String header; + + @Autowired + private RedisCache redisCache; + + @SuppressWarnings("unchecked") + @Override + public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) + { + String nowParams = ""; + if (request instanceof RepeatedlyRequestWrapper) + { + RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request; + nowParams = HttpHelper.getBodyString(repeatedlyRequest); + } + + // body参数为空,获取Parameter的数据 + if (StringUtils.isEmpty(nowParams)) + { + nowParams = JSON.toJSONString(request.getParameterMap()); + } + Map nowDataMap = new HashMap(); + nowDataMap.put(REPEAT_PARAMS, nowParams); + nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); + + // 请求地址(作为存放cache的key值) + String url = request.getRequestURI(); + + // 唯一值(没有消息头则使用请求地址) + String submitKey = StringUtils.trimToEmpty(request.getHeader(header)); + + // 唯一标识(指定key + url + 消息头) + String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey; + + Object sessionObj = redisCache.getCacheObject(cacheRepeatKey); + if (sessionObj != null) + { + Map sessionMap = (Map) sessionObj; + if (sessionMap.containsKey(url)) + { + Map preDataMap = (Map) sessionMap.get(url); + if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval())) + { + return true; + } + } + } + Map cacheMap = new HashMap(); + cacheMap.put(url, nowDataMap); + redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS); + return false; + } + + /** + * 判断参数是否相同 + */ + private boolean compareParams(Map nowMap, Map preMap) + { + String nowParams = (String) nowMap.get(REPEAT_PARAMS); + String preParams = (String) preMap.get(REPEAT_PARAMS); + return nowParams.equals(preParams); + } + + /** + * 判断两次间隔时间 + */ + private boolean compareTime(Map nowMap, Map preMap, int interval) + { + long time1 = (Long) nowMap.get(REPEAT_TIME); + long time2 = (Long) preMap.get(REPEAT_TIME); + if ((time1 - time2) < interval) + { + return true; + } + return false; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/AsyncManager.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/AsyncManager.java new file mode 100644 index 0000000..7387a02 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/AsyncManager.java @@ -0,0 +1,55 @@ +package com.ruoyi.framework.manager; + +import java.util.TimerTask; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import com.ruoyi.common.utils.Threads; +import com.ruoyi.common.utils.spring.SpringUtils; + +/** + * 异步任务管理器 + * + * @author ruoyi + */ +public class AsyncManager +{ + /** + * 操作延迟10毫秒 + */ + private final int OPERATE_DELAY_TIME = 10; + + /** + * 异步操作任务调度线程池 + */ + private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService"); + + /** + * 单例模式 + */ + private AsyncManager(){} + + private static AsyncManager me = new AsyncManager(); + + public static AsyncManager me() + { + return me; + } + + /** + * 执行任务 + * + * @param task 任务 + */ + public void execute(TimerTask task) + { + executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS); + } + + /** + * 停止任务线程池 + */ + public void shutdown() + { + Threads.shutdownAndAwaitTermination(executor); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/ShutdownManager.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/ShutdownManager.java new file mode 100644 index 0000000..e36ca3c --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/ShutdownManager.java @@ -0,0 +1,39 @@ +package com.ruoyi.framework.manager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import javax.annotation.PreDestroy; + +/** + * 确保应用退出时能关闭后台线程 + * + * @author ruoyi + */ +@Component +public class ShutdownManager +{ + private static final Logger logger = LoggerFactory.getLogger("sys-user"); + + @PreDestroy + public void destroy() + { + shutdownAsyncManager(); + } + + /** + * 停止异步执行任务 + */ + private void shutdownAsyncManager() + { + try + { + logger.info("====关闭后台任务任务线程池===="); + AsyncManager.me().shutdown(); + } + catch (Exception e) + { + logger.error(e.getMessage(), e); + } + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/factory/AsyncFactory.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/factory/AsyncFactory.java new file mode 100644 index 0000000..267e305 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/factory/AsyncFactory.java @@ -0,0 +1,102 @@ +package com.ruoyi.framework.manager.factory; + +import java.util.TimerTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.utils.LogUtils; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.AddressUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.system.domain.SysLogininfor; +import com.ruoyi.system.domain.SysOperLog; +import com.ruoyi.system.service.ISysLogininforService; +import com.ruoyi.system.service.ISysOperLogService; +import eu.bitwalker.useragentutils.UserAgent; + +/** + * 异步工厂(产生任务用) + * + * @author ruoyi + */ +public class AsyncFactory +{ + private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user"); + + /** + * 记录登录信息 + * + * @param username 用户名 + * @param status 状态 + * @param message 消息 + * @param args 列表 + * @return 任务task + */ + public static TimerTask recordLogininfor(final String username, final String status, final String message, + final Object... args) + { + final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent")); + final String ip = IpUtils.getIpAddr(); + return new TimerTask() + { + @Override + public void run() + { + String address = AddressUtils.getRealAddressByIP(ip); + StringBuilder s = new StringBuilder(); + s.append(LogUtils.getBlock(ip)); + s.append(address); + s.append(LogUtils.getBlock(username)); + s.append(LogUtils.getBlock(status)); + s.append(LogUtils.getBlock(message)); + // 打印信息到日志 + sys_user_logger.info(s.toString(), args); + // 获取客户端操作系统 + String os = userAgent.getOperatingSystem().getName(); + // 获取客户端浏览器 + String browser = userAgent.getBrowser().getName(); + // 封装对象 + SysLogininfor logininfor = new SysLogininfor(); + logininfor.setUserName(username); + logininfor.setIpaddr(ip); + logininfor.setLoginLocation(address); + logininfor.setBrowser(browser); + logininfor.setOs(os); + logininfor.setMsg(message); + // 日志状态 + if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER)) + { + logininfor.setStatus(Constants.SUCCESS); + } + else if (Constants.LOGIN_FAIL.equals(status)) + { + logininfor.setStatus(Constants.FAIL); + } + // 插入数据 + SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor); + } + }; + } + + /** + * 操作日志记录 + * + * @param operLog 操作日志信息 + * @return 任务task + */ + public static TimerTask recordOper(final SysOperLog operLog) + { + return new TimerTask() + { + @Override + public void run() + { + // 远程查询操作地点 + operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp())); + SpringUtils.getBean(ISysOperLogService.class).insertOperlog(operLog); + } + }; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java new file mode 100644 index 0000000..6c776ce --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java @@ -0,0 +1,28 @@ +package com.ruoyi.framework.security.context; + +import org.springframework.security.core.Authentication; + +/** + * 身份验证信息 + * + * @author ruoyi + */ +public class AuthenticationContextHolder +{ + private static final ThreadLocal contextHolder = new ThreadLocal<>(); + + public static Authentication getContext() + { + return contextHolder.get(); + } + + public static void setContext(Authentication context) + { + contextHolder.set(context); + } + + public static void clearContext() + { + contextHolder.remove(); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/PermissionContextHolder.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/PermissionContextHolder.java new file mode 100644 index 0000000..5472f3d --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/PermissionContextHolder.java @@ -0,0 +1,27 @@ +package com.ruoyi.framework.security.context; + +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import com.ruoyi.common.core.text.Convert; + +/** + * 权限信息 + * + * @author ruoyi + */ +public class PermissionContextHolder +{ + private static final String PERMISSION_CONTEXT_ATTRIBUTES = "PERMISSION_CONTEXT"; + + public static void setContext(String permission) + { + RequestContextHolder.currentRequestAttributes().setAttribute(PERMISSION_CONTEXT_ATTRIBUTES, permission, + RequestAttributes.SCOPE_REQUEST); + } + + public static String getContext() + { + return Convert.toStr(RequestContextHolder.currentRequestAttributes().getAttribute(PERMISSION_CONTEXT_ATTRIBUTES, + RequestAttributes.SCOPE_REQUEST)); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java new file mode 100644 index 0000000..3eb2495 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java @@ -0,0 +1,44 @@ +package com.ruoyi.framework.security.filter; + +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.web.service.TokenService; + +/** + * token过滤器 验证token有效性 + * + * @author ruoyi + */ +@Component +public class JwtAuthenticationTokenFilter extends OncePerRequestFilter +{ + @Autowired + private TokenService tokenService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException + { + LoginUser loginUser = tokenService.getLoginUser(request); + if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) + { + tokenService.verifyToken(loginUser); + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + } + chain.doFilter(request, response); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java new file mode 100644 index 0000000..93b7032 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java @@ -0,0 +1,34 @@ +package com.ruoyi.framework.security.handle; + +import java.io.IOException; +import java.io.Serializable; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; + +/** + * 认证失败处理类 返回未授权 + * + * @author ruoyi + */ +@Component +public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable +{ + private static final long serialVersionUID = -8970718410437077606L; + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) + throws IOException + { + int code = HttpStatus.UNAUTHORIZED; + String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI()); + ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg))); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.java new file mode 100644 index 0000000..2f89a91 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.java @@ -0,0 +1,53 @@ +package com.ruoyi.framework.security.handle; + +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.framework.web.service.TokenService; + +/** + * 自定义退出处理类 返回成功 + * + * @author ruoyi + */ +@Configuration +public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler +{ + @Autowired + private TokenService tokenService; + + /** + * 退出处理 + * + * @return + */ + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws IOException, ServletException + { + LoginUser loginUser = tokenService.getLoginUser(request); + if (StringUtils.isNotNull(loginUser)) + { + String userName = loginUser.getUsername(); + // 删除用户缓存记录 + tokenService.delLoginUser(loginUser.getToken()); + // 记录用户退出日志 + AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, MessageUtils.message("user.logout.success"))); + } + ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success(MessageUtils.message("user.logout.success")))); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/Server.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/Server.java new file mode 100644 index 0000000..63b03da --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/Server.java @@ -0,0 +1,240 @@ +package com.ruoyi.framework.web.domain; + +import java.net.UnknownHostException; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import com.ruoyi.common.utils.Arith; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.framework.web.domain.server.Cpu; +import com.ruoyi.framework.web.domain.server.Jvm; +import com.ruoyi.framework.web.domain.server.Mem; +import com.ruoyi.framework.web.domain.server.Sys; +import com.ruoyi.framework.web.domain.server.SysFile; +import oshi.SystemInfo; +import oshi.hardware.CentralProcessor; +import oshi.hardware.CentralProcessor.TickType; +import oshi.hardware.GlobalMemory; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.software.os.FileSystem; +import oshi.software.os.OSFileStore; +import oshi.software.os.OperatingSystem; +import oshi.util.Util; + +/** + * 服务器相关信息 + * + * @author ruoyi + */ +public class Server +{ + private static final int OSHI_WAIT_SECOND = 1000; + + /** + * CPU相关信息 + */ + private Cpu cpu = new Cpu(); + + /** + * 內存相关信息 + */ + private Mem mem = new Mem(); + + /** + * JVM相关信息 + */ + private Jvm jvm = new Jvm(); + + /** + * 服务器相关信息 + */ + private Sys sys = new Sys(); + + /** + * 磁盘相关信息 + */ + private List sysFiles = new LinkedList(); + + public Cpu getCpu() + { + return cpu; + } + + public void setCpu(Cpu cpu) + { + this.cpu = cpu; + } + + public Mem getMem() + { + return mem; + } + + public void setMem(Mem mem) + { + this.mem = mem; + } + + public Jvm getJvm() + { + return jvm; + } + + public void setJvm(Jvm jvm) + { + this.jvm = jvm; + } + + public Sys getSys() + { + return sys; + } + + public void setSys(Sys sys) + { + this.sys = sys; + } + + public List getSysFiles() + { + return sysFiles; + } + + public void setSysFiles(List sysFiles) + { + this.sysFiles = sysFiles; + } + + public void copyTo() throws Exception + { + SystemInfo si = new SystemInfo(); + HardwareAbstractionLayer hal = si.getHardware(); + + setCpuInfo(hal.getProcessor()); + + setMemInfo(hal.getMemory()); + + setSysInfo(); + + setJvmInfo(); + + setSysFiles(si.getOperatingSystem()); + } + + /** + * 设置CPU信息 + */ + private void setCpuInfo(CentralProcessor processor) + { + // CPU信息 + long[] prevTicks = processor.getSystemCpuLoadTicks(); + Util.sleep(OSHI_WAIT_SECOND); + long[] ticks = processor.getSystemCpuLoadTicks(); + long nice = ticks[TickType.NICE.getIndex()] - prevTicks[TickType.NICE.getIndex()]; + long irq = ticks[TickType.IRQ.getIndex()] - prevTicks[TickType.IRQ.getIndex()]; + long softirq = ticks[TickType.SOFTIRQ.getIndex()] - prevTicks[TickType.SOFTIRQ.getIndex()]; + long steal = ticks[TickType.STEAL.getIndex()] - prevTicks[TickType.STEAL.getIndex()]; + long cSys = ticks[TickType.SYSTEM.getIndex()] - prevTicks[TickType.SYSTEM.getIndex()]; + long user = ticks[TickType.USER.getIndex()] - prevTicks[TickType.USER.getIndex()]; + long iowait = ticks[TickType.IOWAIT.getIndex()] - prevTicks[TickType.IOWAIT.getIndex()]; + long idle = ticks[TickType.IDLE.getIndex()] - prevTicks[TickType.IDLE.getIndex()]; + long totalCpu = user + nice + cSys + idle + iowait + irq + softirq + steal; + cpu.setCpuNum(processor.getLogicalProcessorCount()); + cpu.setTotal(totalCpu); + cpu.setSys(cSys); + cpu.setUsed(user); + cpu.setWait(iowait); + cpu.setFree(idle); + } + + /** + * 设置内存信息 + */ + private void setMemInfo(GlobalMemory memory) + { + mem.setTotal(memory.getTotal()); + mem.setUsed(memory.getTotal() - memory.getAvailable()); + mem.setFree(memory.getAvailable()); + } + + /** + * 设置服务器信息 + */ + private void setSysInfo() + { + Properties props = System.getProperties(); + sys.setComputerName(IpUtils.getHostName()); + sys.setComputerIp(IpUtils.getHostIp()); + sys.setOsName(props.getProperty("os.name")); + sys.setOsArch(props.getProperty("os.arch")); + sys.setUserDir(props.getProperty("user.dir")); + } + + /** + * 设置Java虚拟机 + */ + private void setJvmInfo() throws UnknownHostException + { + Properties props = System.getProperties(); + jvm.setTotal(Runtime.getRuntime().totalMemory()); + jvm.setMax(Runtime.getRuntime().maxMemory()); + jvm.setFree(Runtime.getRuntime().freeMemory()); + jvm.setVersion(props.getProperty("java.version")); + jvm.setHome(props.getProperty("java.home")); + } + + /** + * 设置磁盘信息 + */ + private void setSysFiles(OperatingSystem os) + { + FileSystem fileSystem = os.getFileSystem(); + List fsArray = fileSystem.getFileStores(); + for (OSFileStore fs : fsArray) + { + long free = fs.getUsableSpace(); + long total = fs.getTotalSpace(); + long used = total - free; + SysFile sysFile = new SysFile(); + sysFile.setDirName(fs.getMount()); + sysFile.setSysTypeName(fs.getType()); + sysFile.setTypeName(fs.getName()); + sysFile.setTotal(convertFileSize(total)); + sysFile.setFree(convertFileSize(free)); + sysFile.setUsed(convertFileSize(used)); + sysFile.setUsage(Arith.mul(Arith.div(used, total, 4), 100)); + sysFiles.add(sysFile); + } + } + + /** + * 字节转换 + * + * @param size 字节大小 + * @return 转换后值 + */ + public String convertFileSize(long size) + { + long kb = 1024; + long mb = kb * 1024; + long gb = mb * 1024; + if (size >= gb) + { + return String.format("%.1f GB", (float) size / gb); + } + else if (size >= mb) + { + float f = (float) size / mb; + return String.format(f > 100 ? "%.0f MB" : "%.1f MB", f); + } + else if (size >= kb) + { + float f = (float) size / kb; + return String.format(f > 100 ? "%.0f KB" : "%.1f KB", f); + } + else + { + return String.format("%d B", size); + } + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Cpu.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Cpu.java new file mode 100644 index 0000000..a13a66c --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Cpu.java @@ -0,0 +1,101 @@ +package com.ruoyi.framework.web.domain.server; + +import com.ruoyi.common.utils.Arith; + +/** + * CPU相关信息 + * + * @author ruoyi + */ +public class Cpu +{ + /** + * 核心数 + */ + private int cpuNum; + + /** + * CPU总的使用率 + */ + private double total; + + /** + * CPU系统使用率 + */ + private double sys; + + /** + * CPU用户使用率 + */ + private double used; + + /** + * CPU当前等待率 + */ + private double wait; + + /** + * CPU当前空闲率 + */ + private double free; + + public int getCpuNum() + { + return cpuNum; + } + + public void setCpuNum(int cpuNum) + { + this.cpuNum = cpuNum; + } + + public double getTotal() + { + return Arith.round(Arith.mul(total, 100), 2); + } + + public void setTotal(double total) + { + this.total = total; + } + + public double getSys() + { + return Arith.round(Arith.mul(sys / total, 100), 2); + } + + public void setSys(double sys) + { + this.sys = sys; + } + + public double getUsed() + { + return Arith.round(Arith.mul(used / total, 100), 2); + } + + public void setUsed(double used) + { + this.used = used; + } + + public double getWait() + { + return Arith.round(Arith.mul(wait / total, 100), 2); + } + + public void setWait(double wait) + { + this.wait = wait; + } + + public double getFree() + { + return Arith.round(Arith.mul(free / total, 100), 2); + } + + public void setFree(double free) + { + this.free = free; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Jvm.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Jvm.java new file mode 100644 index 0000000..1fdc6ac --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Jvm.java @@ -0,0 +1,130 @@ +package com.ruoyi.framework.web.domain.server; + +import java.lang.management.ManagementFactory; +import com.ruoyi.common.utils.Arith; +import com.ruoyi.common.utils.DateUtils; + +/** + * JVM相关信息 + * + * @author ruoyi + */ +public class Jvm +{ + /** + * 当前JVM占用的内存总数(M) + */ + private double total; + + /** + * JVM最大可用内存总数(M) + */ + private double max; + + /** + * JVM空闲内存(M) + */ + private double free; + + /** + * JDK版本 + */ + private String version; + + /** + * JDK路径 + */ + private String home; + + public double getTotal() + { + return Arith.div(total, (1024 * 1024), 2); + } + + public void setTotal(double total) + { + this.total = total; + } + + public double getMax() + { + return Arith.div(max, (1024 * 1024), 2); + } + + public void setMax(double max) + { + this.max = max; + } + + public double getFree() + { + return Arith.div(free, (1024 * 1024), 2); + } + + public void setFree(double free) + { + this.free = free; + } + + public double getUsed() + { + return Arith.div(total - free, (1024 * 1024), 2); + } + + public double getUsage() + { + return Arith.mul(Arith.div(total - free, total, 4), 100); + } + + /** + * 获取JDK名称 + */ + public String getName() + { + return ManagementFactory.getRuntimeMXBean().getVmName(); + } + + public String getVersion() + { + return version; + } + + public void setVersion(String version) + { + this.version = version; + } + + public String getHome() + { + return home; + } + + public void setHome(String home) + { + this.home = home; + } + + /** + * JDK启动时间 + */ + public String getStartTime() + { + return DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, DateUtils.getServerStartDate()); + } + + /** + * JDK运行时间 + */ + public String getRunTime() + { + return DateUtils.timeDistance(DateUtils.getNowDate(), DateUtils.getServerStartDate()); + } + + /** + * 运行参数 + */ + public String getInputArgs() + { + return ManagementFactory.getRuntimeMXBean().getInputArguments().toString(); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Mem.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Mem.java new file mode 100644 index 0000000..13eec52 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Mem.java @@ -0,0 +1,61 @@ +package com.ruoyi.framework.web.domain.server; + +import com.ruoyi.common.utils.Arith; + +/** + * 內存相关信息 + * + * @author ruoyi + */ +public class Mem +{ + /** + * 内存总量 + */ + private double total; + + /** + * 已用内存 + */ + private double used; + + /** + * 剩余内存 + */ + private double free; + + public double getTotal() + { + return Arith.div(total, (1024 * 1024 * 1024), 2); + } + + public void setTotal(long total) + { + this.total = total; + } + + public double getUsed() + { + return Arith.div(used, (1024 * 1024 * 1024), 2); + } + + public void setUsed(long used) + { + this.used = used; + } + + public double getFree() + { + return Arith.div(free, (1024 * 1024 * 1024), 2); + } + + public void setFree(long free) + { + this.free = free; + } + + public double getUsage() + { + return Arith.mul(Arith.div(used, total, 4), 100); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Sys.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Sys.java new file mode 100644 index 0000000..45d64d9 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Sys.java @@ -0,0 +1,84 @@ +package com.ruoyi.framework.web.domain.server; + +/** + * 系统相关信息 + * + * @author ruoyi + */ +public class Sys +{ + /** + * 服务器名称 + */ + private String computerName; + + /** + * 服务器Ip + */ + private String computerIp; + + /** + * 项目路径 + */ + private String userDir; + + /** + * 操作系统 + */ + private String osName; + + /** + * 系统架构 + */ + private String osArch; + + public String getComputerName() + { + return computerName; + } + + public void setComputerName(String computerName) + { + this.computerName = computerName; + } + + public String getComputerIp() + { + return computerIp; + } + + public void setComputerIp(String computerIp) + { + this.computerIp = computerIp; + } + + public String getUserDir() + { + return userDir; + } + + public void setUserDir(String userDir) + { + this.userDir = userDir; + } + + public String getOsName() + { + return osName; + } + + public void setOsName(String osName) + { + this.osName = osName; + } + + public String getOsArch() + { + return osArch; + } + + public void setOsArch(String osArch) + { + this.osArch = osArch; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/SysFile.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/SysFile.java new file mode 100644 index 0000000..1320cde --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/SysFile.java @@ -0,0 +1,114 @@ +package com.ruoyi.framework.web.domain.server; + +/** + * 系统文件相关信息 + * + * @author ruoyi + */ +public class SysFile +{ + /** + * 盘符路径 + */ + private String dirName; + + /** + * 盘符类型 + */ + private String sysTypeName; + + /** + * 文件类型 + */ + private String typeName; + + /** + * 总大小 + */ + private String total; + + /** + * 剩余大小 + */ + private String free; + + /** + * 已经使用量 + */ + private String used; + + /** + * 资源的使用率 + */ + private double usage; + + public String getDirName() + { + return dirName; + } + + public void setDirName(String dirName) + { + this.dirName = dirName; + } + + public String getSysTypeName() + { + return sysTypeName; + } + + public void setSysTypeName(String sysTypeName) + { + this.sysTypeName = sysTypeName; + } + + public String getTypeName() + { + return typeName; + } + + public void setTypeName(String typeName) + { + this.typeName = typeName; + } + + public String getTotal() + { + return total; + } + + public void setTotal(String total) + { + this.total = total; + } + + public String getFree() + { + return free; + } + + public void setFree(String free) + { + this.free = free; + } + + public String getUsed() + { + return used; + } + + public void setUsed(String used) + { + this.used = used; + } + + public double getUsage() + { + return usage; + } + + public void setUsage(double usage) + { + this.usage = usage; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..3cb17d6 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java @@ -0,0 +1,145 @@ +package com.ruoyi.framework.web.exception; + +import javax.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.validation.BindException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingPathVariableException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.exception.DemoModeException; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.html.EscapeUtil; + +/** + * 全局异常处理器 + * + * @author ruoyi + */ +@RestControllerAdvice +public class GlobalExceptionHandler +{ + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + /** + * 权限校验异常 + */ + @ExceptionHandler(AccessDeniedException.class) + public AjaxResult handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage()); + return AjaxResult.error(HttpStatus.FORBIDDEN, "没有权限,请联系管理员授权"); + } + + /** + * 请求方式不支持 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e, + HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod()); + return AjaxResult.error(e.getMessage()); + } + + /** + * 业务异常 + */ + @ExceptionHandler(ServiceException.class) + public AjaxResult handleServiceException(ServiceException e, HttpServletRequest request) + { + log.error(e.getMessage(), e); + Integer code = e.getCode(); + return StringUtils.isNotNull(code) ? AjaxResult.error(code, e.getMessage()) : AjaxResult.error(e.getMessage()); + } + + /** + * 请求路径中缺少必需的路径变量 + */ + @ExceptionHandler(MissingPathVariableException.class) + public AjaxResult handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求路径中缺少必需的路径变量'{}',发生系统异常.", requestURI, e); + return AjaxResult.error(String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName())); + } + + /** + * 请求参数类型不匹配 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public AjaxResult handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + String value = Convert.toStr(e.getValue()); + if (StringUtils.isNotEmpty(value)) + { + value = EscapeUtil.clean(value); + } + log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI, e); + return AjaxResult.error(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), value)); + } + + /** + * 拦截未知的运行时异常 + */ + @ExceptionHandler(RuntimeException.class) + public AjaxResult handleRuntimeException(RuntimeException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',发生未知异常.", requestURI, e); + return AjaxResult.error(e.getMessage()); + } + + /** + * 系统异常 + */ + @ExceptionHandler(Exception.class) + public AjaxResult handleException(Exception e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',发生系统异常.", requestURI, e); + return AjaxResult.error(e.getMessage()); + } + + /** + * 自定义验证异常 + */ + @ExceptionHandler(BindException.class) + public AjaxResult handleBindException(BindException e) + { + log.error(e.getMessage(), e); + String message = e.getAllErrors().get(0).getDefaultMessage(); + return AjaxResult.error(message); + } + + /** + * 自定义验证异常 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e) + { + log.error(e.getMessage(), e); + String message = e.getBindingResult().getFieldError().getDefaultMessage(); + return AjaxResult.error(message); + } + + /** + * 演示模式异常 + */ + @ExceptionHandler(DemoModeException.class) + public AjaxResult handleDemoModeException(DemoModeException e) + { + return AjaxResult.error("演示模式,不允许操作"); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.java new file mode 100644 index 0000000..07d259a --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.java @@ -0,0 +1,159 @@ +package com.ruoyi.framework.web.service; + +import java.util.Set; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.security.context.PermissionContextHolder; + +/** + * RuoYi首创 自定义权限实现,ss取自SpringSecurity首字母 + * + * @author ruoyi + */ +@Service("ss") +public class PermissionService +{ + /** + * 验证用户是否具备某权限 + * + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + public boolean hasPermi(String permission) + { + if (StringUtils.isEmpty(permission)) + { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) + { + return false; + } + PermissionContextHolder.setContext(permission); + return hasPermissions(loginUser.getPermissions(), permission); + } + + /** + * 验证用户是否不具备某权限,与 hasPermi逻辑相反 + * + * @param permission 权限字符串 + * @return 用户是否不具备某权限 + */ + public boolean lacksPermi(String permission) + { + return hasPermi(permission) != true; + } + + /** + * 验证用户是否具有以下任意一个权限 + * + * @param permissions 以 PERMISSION_DELIMETER 为分隔符的权限列表 + * @return 用户是否具有以下任意一个权限 + */ + public boolean hasAnyPermi(String permissions) + { + if (StringUtils.isEmpty(permissions)) + { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) + { + return false; + } + PermissionContextHolder.setContext(permissions); + Set authorities = loginUser.getPermissions(); + for (String permission : permissions.split(Constants.PERMISSION_DELIMETER)) + { + if (permission != null && hasPermissions(authorities, permission)) + { + return true; + } + } + return false; + } + + /** + * 判断用户是否拥有某个角色 + * + * @param role 角色字符串 + * @return 用户是否具备某角色 + */ + public boolean hasRole(String role) + { + if (StringUtils.isEmpty(role)) + { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) + { + return false; + } + for (SysRole sysRole : loginUser.getUser().getRoles()) + { + String roleKey = sysRole.getRoleKey(); + if (Constants.SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(role))) + { + return true; + } + } + return false; + } + + /** + * 验证用户是否不具备某角色,与 isRole逻辑相反。 + * + * @param role 角色名称 + * @return 用户是否不具备某角色 + */ + public boolean lacksRole(String role) + { + return hasRole(role) != true; + } + + /** + * 验证用户是否具有以下任意一个角色 + * + * @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表 + * @return 用户是否具有以下任意一个角色 + */ + public boolean hasAnyRoles(String roles) + { + if (StringUtils.isEmpty(roles)) + { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) + { + return false; + } + for (String role : roles.split(Constants.ROLE_DELIMETER)) + { + if (hasRole(role)) + { + return true; + } + } + return false; + } + + /** + * 判断是否包含权限 + * + * @param permissions 权限列表 + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + private boolean hasPermissions(Set permissions, String permission) + { + return permissions.contains(Constants.ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission)); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java new file mode 100644 index 0000000..fe16427 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java @@ -0,0 +1,181 @@ +package com.ruoyi.framework.web.service; + +import javax.annotation.Resource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.exception.user.BlackListException; +import com.ruoyi.common.exception.user.CaptchaException; +import com.ruoyi.common.exception.user.CaptchaExpireException; +import com.ruoyi.common.exception.user.UserNotExistsException; +import com.ruoyi.common.exception.user.UserPasswordNotMatchException; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.framework.security.context.AuthenticationContextHolder; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.system.service.ISysUserService; + +/** + * 登录校验方法 + * + * @author ruoyi + */ +@Component +public class SysLoginService +{ + @Autowired + private TokenService tokenService; + + @Resource + private AuthenticationManager authenticationManager; + + @Autowired + private RedisCache redisCache; + + @Autowired + private ISysUserService userService; + + @Autowired + private ISysConfigService configService; + + /** + * 登录验证 + * + * @param username 用户名 + * @param password 密码 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public String login(String username, String password, String code, String uuid) + { + // 验证码校验 + validateCaptcha(username, code, uuid); + // 登录前置校验 + loginPreCheck(username, password); + // 用户验证 + Authentication authentication = null; + try + { + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); + AuthenticationContextHolder.setContext(authenticationToken); + // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername + authentication = authenticationManager.authenticate(authenticationToken); + } + catch (Exception e) + { + if (e instanceof BadCredentialsException) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); + throw new UserPasswordNotMatchException(); + } + else + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage())); + throw new ServiceException(e.getMessage()); + } + } + finally + { + AuthenticationContextHolder.clearContext(); + } + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); + LoginUser loginUser = (LoginUser) authentication.getPrincipal(); + recordLoginInfo(loginUser.getUserId()); + // 生成token + return tokenService.createToken(loginUser); + } + + /** + * 校验验证码 + * + * @param username 用户名 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public void validateCaptcha(String username, String code, String uuid) + { + boolean captchaEnabled = configService.selectCaptchaEnabled(); + if (captchaEnabled) + { + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); + String captcha = redisCache.getCacheObject(verifyKey); + if (captcha == null) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"))); + throw new CaptchaExpireException(); + } + redisCache.deleteObject(verifyKey); + if (!code.equalsIgnoreCase(captcha)) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"))); + throw new CaptchaException(); + } + } + } + + /** + * 登录前置校验 + * @param username 用户名 + * @param password 用户密码 + */ + public void loginPreCheck(String username, String password) + { + // 用户名或密码为空 错误 + if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null"))); + throw new UserNotExistsException(); + } + // 密码如果不在指定范围内 错误 + if (password.length() < UserConstants.PASSWORD_MIN_LENGTH + || password.length() > UserConstants.PASSWORD_MAX_LENGTH) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); + throw new UserPasswordNotMatchException(); + } + // 用户名不在指定范围内 错误 + if (username.length() < UserConstants.USERNAME_MIN_LENGTH + || username.length() > UserConstants.USERNAME_MAX_LENGTH) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); + throw new UserPasswordNotMatchException(); + } + // IP黑名单校验 + String blackStr = configService.selectConfigByKey("sys.login.blackIPList"); + if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr())) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("login.blocked"))); + throw new BlackListException(); + } + } + + /** + * 记录登录信息 + * + * @param userId 用户ID + */ + public void recordLoginInfo(Long userId) + { + SysUser sysUser = new SysUser(); + sysUser.setUserId(userId); + sysUser.setLoginIp(IpUtils.getIpAddr()); + sysUser.setLoginDate(DateUtils.getNowDate()); + userService.updateUserProfile(sysUser); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java new file mode 100644 index 0000000..6728c7b --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java @@ -0,0 +1,86 @@ +package com.ruoyi.framework.web.service; + +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.exception.user.UserPasswordNotMatchException; +import com.ruoyi.common.exception.user.UserPasswordRetryLimitExceedException; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.framework.security.context.AuthenticationContextHolder; + +/** + * 登录密码方法 + * + * @author ruoyi + */ +@Component +public class SysPasswordService +{ + @Autowired + private RedisCache redisCache; + + @Value(value = "${user.password.maxRetryCount}") + private int maxRetryCount; + + @Value(value = "${user.password.lockTime}") + private int lockTime; + + /** + * 登录账户密码错误次数缓存键名 + * + * @param username 用户名 + * @return 缓存键key + */ + private String getCacheKey(String username) + { + return CacheConstants.PWD_ERR_CNT_KEY + username; + } + + public void validate(SysUser user) + { + Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext(); + String username = usernamePasswordAuthenticationToken.getName(); + String password = usernamePasswordAuthenticationToken.getCredentials().toString(); + + Integer retryCount = redisCache.getCacheObject(getCacheKey(username)); + + if (retryCount == null) + { + retryCount = 0; + } + + if (retryCount >= Integer.valueOf(maxRetryCount).intValue()) + { + throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime); + } + + if (!matches(user, password)) + { + retryCount = retryCount + 1; + redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES); + throw new UserPasswordNotMatchException(); + } + else + { + clearLoginRecordCache(username); + } + } + + public boolean matches(SysUser user, String rawPassword) + { + return SecurityUtils.matchesPassword(rawPassword, user.getPassword()); + } + + public void clearLoginRecordCache(String loginName) + { + if (redisCache.hasKey(getCacheKey(loginName))) + { + redisCache.deleteObject(getCacheKey(loginName)); + } + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPermissionService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPermissionService.java new file mode 100644 index 0000000..c4d0fa5 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPermissionService.java @@ -0,0 +1,88 @@ +package com.ruoyi.framework.web.service; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.service.ISysMenuService; +import com.ruoyi.system.service.ISysRoleService; + +/** + * 用户权限处理 + * + * @author ruoyi + */ +@Component +public class SysPermissionService +{ + @Autowired + private ISysRoleService roleService; + + @Autowired + private ISysMenuService menuService; + + /** + * 获取角色数据权限 + * + * @param user 用户信息 + * @return 角色权限信息 + */ + public Set getRolePermission(SysUser user) + { + Set roles = new HashSet(); + // 管理员拥有所有权限 + if (user.isAdmin()) + { + roles.add("admin"); + } + else + { + roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId())); + } + return roles; + } + + /** + * 获取菜单数据权限 + * + * @param user 用户信息 + * @return 菜单权限信息 + */ + public Set getMenuPermission(SysUser user) + { + Set perms = new HashSet(); + // 管理员拥有所有权限 + if (user.isAdmin()) + { + perms.add("*:*:*"); + } + else + { + List roles = user.getRoles(); + if (!CollectionUtils.isEmpty(roles)) + { + // 多角色设置permissions属性,以便数据权限匹配权限 + for (SysRole role : roles) + { + if (StringUtils.equals(role.getStatus(), UserConstants.ROLE_NORMAL) && !role.isAdmin()) + { + Set rolePerms = menuService.selectMenuPermsByRoleId(role.getRoleId()); + role.setPermissions(rolePerms); + perms.addAll(rolePerms); + } + } + } + else + { + perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId())); + } + } + return perms; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java new file mode 100644 index 0000000..8305bcb --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java @@ -0,0 +1,117 @@ +package com.ruoyi.framework.web.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.RegisterBody; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.exception.user.CaptchaException; +import com.ruoyi.common.exception.user.CaptchaExpireException; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.system.service.ISysUserService; + +/** + * 注册校验方法 + * + * @author ruoyi + */ +@Component +public class SysRegisterService +{ + @Autowired + private ISysUserService userService; + + @Autowired + private ISysConfigService configService; + + @Autowired + private RedisCache redisCache; + + /** + * 注册 + */ + public String register(RegisterBody registerBody) + { + String msg = "", username = registerBody.getUsername(), password = registerBody.getPassword(); + SysUser sysUser = new SysUser(); + sysUser.setUserName(username); + + // 验证码开关 + boolean captchaEnabled = configService.selectCaptchaEnabled(); + if (captchaEnabled) + { + validateCaptcha(username, registerBody.getCode(), registerBody.getUuid()); + } + + if (StringUtils.isEmpty(username)) + { + msg = "用户名不能为空"; + } + else if (StringUtils.isEmpty(password)) + { + msg = "用户密码不能为空"; + } + else if (username.length() < UserConstants.USERNAME_MIN_LENGTH + || username.length() > UserConstants.USERNAME_MAX_LENGTH) + { + msg = "账户长度必须在2到20个字符之间"; + } + else if (password.length() < UserConstants.PASSWORD_MIN_LENGTH + || password.length() > UserConstants.PASSWORD_MAX_LENGTH) + { + msg = "密码长度必须在5到20个字符之间"; + } + else if (!userService.checkUserNameUnique(sysUser)) + { + msg = "保存用户'" + username + "'失败,注册账号已存在"; + } + else + { + sysUser.setNickName(username); + sysUser.setPwdUpdateDate(DateUtils.getNowDate()); + sysUser.setPassword(SecurityUtils.encryptPassword(password)); + boolean regFlag = userService.registerUser(sysUser); + if (!regFlag) + { + msg = "注册失败,请联系系统管理人员"; + } + else + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.REGISTER, MessageUtils.message("user.register.success"))); + } + } + return msg; + } + + /** + * 校验验证码 + * + * @param username 用户名 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public void validateCaptcha(String username, String code, String uuid) + { + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); + String captcha = redisCache.getCacheObject(verifyKey); + redisCache.deleteObject(verifyKey); + if (captcha == null) + { + throw new CaptchaExpireException(); + } + if (!code.equalsIgnoreCase(captcha)) + { + throw new CaptchaException(); + } + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java new file mode 100644 index 0000000..680cab6 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java @@ -0,0 +1,232 @@ +package com.ruoyi.framework.web.service; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.utils.ServletUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.ip.AddressUtils; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.common.utils.uuid.IdUtils; +import eu.bitwalker.useragentutils.UserAgent; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +/** + * token验证处理 + * + * @author ruoyi + */ +@Component +public class TokenService +{ + private static final Logger log = LoggerFactory.getLogger(TokenService.class); + + // 令牌自定义标识 + @Value("${token.header}") + private String header; + + // 令牌秘钥 + @Value("${token.secret}") + private String secret; + + // 令牌有效期(默认30分钟) + @Value("${token.expireTime}") + private int expireTime; + + protected static final long MILLIS_SECOND = 1000; + + protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND; + + private static final Long MILLIS_MINUTE_TWENTY = 20 * 60 * 1000L; + + @Autowired + private RedisCache redisCache; + + /** + * 获取用户身份信息 + * + * @return 用户信息 + */ + public LoginUser getLoginUser(HttpServletRequest request) + { + // 获取请求携带的令牌 + String token = getToken(request); + if (StringUtils.isNotEmpty(token)) + { + try + { + Claims claims = parseToken(token); + // 解析对应的权限以及用户信息 + String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); + String userKey = getTokenKey(uuid); + LoginUser user = redisCache.getCacheObject(userKey); + return user; + } + catch (Exception e) + { + log.error("获取用户信息异常'{}'", e.getMessage()); + } + } + return null; + } + + /** + * 设置用户身份信息 + */ + public void setLoginUser(LoginUser loginUser) + { + if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken())) + { + refreshToken(loginUser); + } + } + + /** + * 删除用户身份信息 + */ + public void delLoginUser(String token) + { + if (StringUtils.isNotEmpty(token)) + { + String userKey = getTokenKey(token); + redisCache.deleteObject(userKey); + } + } + + /** + * 创建令牌 + * + * @param loginUser 用户信息 + * @return 令牌 + */ + public String createToken(LoginUser loginUser) + { + String token = IdUtils.fastUUID(); + loginUser.setToken(token); + setUserAgent(loginUser); + refreshToken(loginUser); + + Map claims = new HashMap<>(); + claims.put(Constants.LOGIN_USER_KEY, token); + claims.put(Constants.JWT_USERNAME, loginUser.getUsername()); + return createToken(claims); + } + + /** + * 验证令牌有效期,相差不足20分钟,自动刷新缓存 + * + * @param loginUser 登录信息 + * @return 令牌 + */ + public void verifyToken(LoginUser loginUser) + { + long expireTime = loginUser.getExpireTime(); + long currentTime = System.currentTimeMillis(); + if (expireTime - currentTime <= MILLIS_MINUTE_TWENTY) + { + refreshToken(loginUser); + } + } + + /** + * 刷新令牌有效期 + * + * @param loginUser 登录信息 + */ + public void refreshToken(LoginUser loginUser) + { + loginUser.setLoginTime(System.currentTimeMillis()); + loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE); + // 根据uuid将loginUser缓存 + String userKey = getTokenKey(loginUser.getToken()); + redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES); + } + + /** + * 设置用户代理信息 + * + * @param loginUser 登录信息 + */ + public void setUserAgent(LoginUser loginUser) + { + UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent")); + String ip = IpUtils.getIpAddr(); + loginUser.setIpaddr(ip); + loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip)); + loginUser.setBrowser(userAgent.getBrowser().getName()); + loginUser.setOs(userAgent.getOperatingSystem().getName()); + } + + /** + * 从数据声明生成令牌 + * + * @param claims 数据声明 + * @return 令牌 + */ + private String createToken(Map claims) + { + String token = Jwts.builder() + .setClaims(claims) + .signWith(SignatureAlgorithm.HS512, secret).compact(); + return token; + } + + /** + * 从令牌中获取数据声明 + * + * @param token 令牌 + * @return 数据声明 + */ + private Claims parseToken(String token) + { + return Jwts.parser() + .setSigningKey(secret) + .parseClaimsJws(token) + .getBody(); + } + + /** + * 从令牌中获取用户名 + * + * @param token 令牌 + * @return 用户名 + */ + public String getUsernameFromToken(String token) + { + Claims claims = parseToken(token); + return claims.getSubject(); + } + + /** + * 获取请求token + * + * @param request + * @return token + */ + private String getToken(HttpServletRequest request) + { + String token = request.getHeader(header); + if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) + { + token = token.replace(Constants.TOKEN_PREFIX, ""); + } + return token; + } + + private String getTokenKey(String uuid) + { + return CacheConstants.LOGIN_TOKEN_KEY + uuid; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java new file mode 100644 index 0000000..5dcdf90 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java @@ -0,0 +1,66 @@ +package com.ruoyi.framework.web.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +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 com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.enums.UserStatus; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.service.ISysUserService; + +/** + * 用户验证处理 + * + * @author ruoyi + */ +@Service +public class UserDetailsServiceImpl implements UserDetailsService +{ + private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class); + + @Autowired + private ISysUserService userService; + + @Autowired + private SysPasswordService passwordService; + + @Autowired + private SysPermissionService permissionService; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException + { + SysUser user = userService.selectUserByUserName(username); + if (StringUtils.isNull(user)) + { + log.info("登录用户:{} 不存在.", username); + throw new ServiceException(MessageUtils.message("user.not.exists")); + } + else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) + { + log.info("登录用户:{} 已被删除.", username); + throw new ServiceException(MessageUtils.message("user.password.delete")); + } + else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) + { + log.info("登录用户:{} 已被停用.", username); + throw new ServiceException(MessageUtils.message("user.blocked")); + } + + passwordService.validate(user); + + return createLoginUser(user); + } + + public UserDetails createLoginUser(SysUser user) + { + return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user)); + } +} diff --git a/ruoyi-generator/pom.xml b/ruoyi-generator/pom.xml new file mode 100644 index 0000000..54b9809 --- /dev/null +++ b/ruoyi-generator/pom.xml @@ -0,0 +1,40 @@ + + + + ruoyi + com.ruoyi + 3.9.0 + + 4.0.0 + + ruoyi-generator + + + generator代码生成 + + + + + + + org.apache.velocity + velocity-engine-core + + + + + com.ruoyi + ruoyi-common + + + + + com.alibaba + druid-spring-boot-starter + + + + + \ No newline at end of file diff --git a/ruoyi-generator/ruoyi-generator.iml b/ruoyi-generator/ruoyi-generator.iml new file mode 100644 index 0000000..15046ed --- /dev/null +++ b/ruoyi-generator/ruoyi-generator.iml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/config/GenConfig.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/config/GenConfig.java new file mode 100644 index 0000000..c01857c --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/config/GenConfig.java @@ -0,0 +1,87 @@ +package com.ruoyi.generator.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Component; + +/** + * 读取代码生成相关配置 + * + * @author ruoyi + */ +@Component +@ConfigurationProperties(prefix = "gen") +@PropertySource(value = { "classpath:generator.yml" }) +public class GenConfig +{ + /** 作者 */ + public static String author; + + /** 生成包路径 */ + public static String packageName; + + /** 自动去除表前缀 */ + public static boolean autoRemovePre; + + /** 表前缀 */ + public static String tablePrefix; + + /** 是否允许生成文件覆盖到本地(自定义路径) */ + public static boolean allowOverwrite; + + public static String getAuthor() + { + return author; + } + + @Value("${author}") + public void setAuthor(String author) + { + GenConfig.author = author; + } + + public static String getPackageName() + { + return packageName; + } + + @Value("${packageName}") + public void setPackageName(String packageName) + { + GenConfig.packageName = packageName; + } + + public static boolean getAutoRemovePre() + { + return autoRemovePre; + } + + @Value("${autoRemovePre}") + public void setAutoRemovePre(boolean autoRemovePre) + { + GenConfig.autoRemovePre = autoRemovePre; + } + + public static String getTablePrefix() + { + return tablePrefix; + } + + @Value("${tablePrefix}") + public void setTablePrefix(String tablePrefix) + { + GenConfig.tablePrefix = tablePrefix; + } + + public static boolean isAllowOverwrite() + { + return allowOverwrite; + } + + @Value("${allowOverwrite}") + public void setAllowOverwrite(boolean allowOverwrite) + { + GenConfig.allowOverwrite = allowOverwrite; + } +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/controller/GenController.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/controller/GenController.java new file mode 100644 index 0000000..1ef0d35 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/controller/GenController.java @@ -0,0 +1,263 @@ +package com.ruoyi.generator.controller; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.io.IOUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.alibaba.druid.DbType; +import com.alibaba.druid.sql.SQLUtils; +import com.alibaba.druid.sql.ast.SQLStatement; +import com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlCreateTableStatement; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.sql.SqlUtil; +import com.ruoyi.generator.config.GenConfig; +import com.ruoyi.generator.domain.GenTable; +import com.ruoyi.generator.domain.GenTableColumn; +import com.ruoyi.generator.service.IGenTableColumnService; +import com.ruoyi.generator.service.IGenTableService; + +/** + * 代码生成 操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/tool/gen") +public class GenController extends BaseController +{ + @Autowired + private IGenTableService genTableService; + + @Autowired + private IGenTableColumnService genTableColumnService; + + /** + * 查询代码生成列表 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:list')") + @GetMapping("/list") + public TableDataInfo genList(GenTable genTable) + { + startPage(); + List list = genTableService.selectGenTableList(genTable); + return getDataTable(list); + } + + /** + * 获取代码生成信息 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:query')") + @GetMapping(value = "/{tableId}") + public AjaxResult getInfo(@PathVariable Long tableId) + { + GenTable table = genTableService.selectGenTableById(tableId); + List tables = genTableService.selectGenTableAll(); + List list = genTableColumnService.selectGenTableColumnListByTableId(tableId); + Map map = new HashMap(); + map.put("info", table); + map.put("rows", list); + map.put("tables", tables); + return success(map); + } + + /** + * 查询数据库列表 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:list')") + @GetMapping("/db/list") + public TableDataInfo dataList(GenTable genTable) + { + startPage(); + List list = genTableService.selectDbTableList(genTable); + return getDataTable(list); + } + + /** + * 查询数据表字段列表 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:list')") + @GetMapping(value = "/column/{tableId}") + public TableDataInfo columnList(Long tableId) + { + TableDataInfo dataInfo = new TableDataInfo(); + List list = genTableColumnService.selectGenTableColumnListByTableId(tableId); + dataInfo.setRows(list); + dataInfo.setTotal(list.size()); + return dataInfo; + } + + /** + * 导入表结构(保存) + */ + @PreAuthorize("@ss.hasPermi('tool:gen:import')") + @Log(title = "代码生成", businessType = BusinessType.IMPORT) + @PostMapping("/importTable") + public AjaxResult importTableSave(String tables) + { + String[] tableNames = Convert.toStrArray(tables); + // 查询表信息 + List tableList = genTableService.selectDbTableListByNames(tableNames); + genTableService.importGenTable(tableList, SecurityUtils.getUsername()); + return success(); + } + + /** + * 创建表结构(保存) + */ + @PreAuthorize("@ss.hasRole('admin')") + @Log(title = "创建表", businessType = BusinessType.OTHER) + @PostMapping("/createTable") + public AjaxResult createTableSave(String sql) + { + try + { + SqlUtil.filterKeyword(sql); + List sqlStatements = SQLUtils.parseStatements(sql, DbType.mysql); + List tableNames = new ArrayList<>(); + for (SQLStatement sqlStatement : sqlStatements) + { + if (sqlStatement instanceof MySqlCreateTableStatement) + { + MySqlCreateTableStatement createTableStatement = (MySqlCreateTableStatement) sqlStatement; + if (genTableService.createTable(createTableStatement.toString())) + { + String tableName = createTableStatement.getTableName().replaceAll("`", ""); + tableNames.add(tableName); + } + } + } + List tableList = genTableService.selectDbTableListByNames(tableNames.toArray(new String[tableNames.size()])); + String operName = SecurityUtils.getUsername(); + genTableService.importGenTable(tableList, operName); + return AjaxResult.success(); + } + catch (Exception e) + { + logger.error(e.getMessage(), e); + return AjaxResult.error("创建表结构异常"); + } + } + + /** + * 修改保存代码生成业务 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:edit')") + @Log(title = "代码生成", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult editSave(@Validated @RequestBody GenTable genTable) + { + genTableService.validateEdit(genTable); + genTableService.updateGenTable(genTable); + return success(); + } + + /** + * 删除代码生成 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:remove')") + @Log(title = "代码生成", businessType = BusinessType.DELETE) + @DeleteMapping("/{tableIds}") + public AjaxResult remove(@PathVariable Long[] tableIds) + { + genTableService.deleteGenTableByIds(tableIds); + return success(); + } + + /** + * 预览代码 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:preview')") + @GetMapping("/preview/{tableId}") + public AjaxResult preview(@PathVariable("tableId") Long tableId) throws IOException + { + Map dataMap = genTableService.previewCode(tableId); + return success(dataMap); + } + + /** + * 生成代码(下载方式) + */ + @PreAuthorize("@ss.hasPermi('tool:gen:code')") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/download/{tableName}") + public void download(HttpServletResponse response, @PathVariable("tableName") String tableName) throws IOException + { + byte[] data = genTableService.downloadCode(tableName); + genCode(response, data); + } + + /** + * 生成代码(自定义路径) + */ + @PreAuthorize("@ss.hasPermi('tool:gen:code')") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/genCode/{tableName}") + public AjaxResult genCode(@PathVariable("tableName") String tableName) + { + if (!GenConfig.isAllowOverwrite()) + { + return AjaxResult.error("【系统预设】不允许生成文件覆盖到本地"); + } + genTableService.generatorCode(tableName); + return success(); + } + + /** + * 同步数据库 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:edit')") + @Log(title = "代码生成", businessType = BusinessType.UPDATE) + @GetMapping("/synchDb/{tableName}") + public AjaxResult synchDb(@PathVariable("tableName") String tableName) + { + genTableService.synchDb(tableName); + return success(); + } + + /** + * 批量生成代码 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:code')") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/batchGenCode") + public void batchGenCode(HttpServletResponse response, String tables) throws IOException + { + String[] tableNames = Convert.toStrArray(tables); + byte[] data = genTableService.downloadCode(tableNames); + genCode(response, data); + } + + /** + * 生成zip文件 + */ + private void genCode(HttpServletResponse response, byte[] data) throws IOException + { + response.reset(); + response.addHeader("Access-Control-Allow-Origin", "*"); + response.addHeader("Access-Control-Expose-Headers", "Content-Disposition"); + response.setHeader("Content-Disposition", "attachment; filename=\"ruoyi.zip\""); + response.addHeader("Content-Length", "" + data.length); + response.setContentType("application/octet-stream; charset=UTF-8"); + IOUtils.write(data, response.getOutputStream()); + } +} \ No newline at end of file diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTable.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTable.java new file mode 100644 index 0000000..022a54d --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTable.java @@ -0,0 +1,385 @@ +package com.ruoyi.generator.domain; + +import java.util.List; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import org.apache.commons.lang3.ArrayUtils; +import com.ruoyi.common.constant.GenConstants; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.utils.StringUtils; + +/** + * 业务表 gen_table + * + * @author ruoyi + */ +public class GenTable extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 编号 */ + private Long tableId; + + /** 表名称 */ + @NotBlank(message = "表名称不能为空") + private String tableName; + + /** 表描述 */ + @NotBlank(message = "表描述不能为空") + private String tableComment; + + /** 关联父表的表名 */ + private String subTableName; + + /** 本表关联父表的外键名 */ + private String subTableFkName; + + /** 实体类名称(首字母大写) */ + @NotBlank(message = "实体类名称不能为空") + private String className; + + /** 使用的模板(crud单表操作 tree树表操作 sub主子表操作) */ + private String tplCategory; + + /** 前端类型(element-ui模版 element-plus模版) */ + private String tplWebType; + + /** 生成包路径 */ + @NotBlank(message = "生成包路径不能为空") + private String packageName; + + /** 生成模块名 */ + @NotBlank(message = "生成模块名不能为空") + private String moduleName; + + /** 生成业务名 */ + @NotBlank(message = "生成业务名不能为空") + private String businessName; + + /** 生成功能名 */ + @NotBlank(message = "生成功能名不能为空") + private String functionName; + + /** 生成作者 */ + @NotBlank(message = "作者不能为空") + private String functionAuthor; + + /** 生成代码方式(0zip压缩包 1自定义路径) */ + private String genType; + + /** 生成路径(不填默认项目路径) */ + private String genPath; + + /** 主键信息 */ + private GenTableColumn pkColumn; + + /** 子表信息 */ + private GenTable subTable; + + /** 表列信息 */ + @Valid + private List columns; + + /** 其它生成选项 */ + private String options; + + /** 树编码字段 */ + private String treeCode; + + /** 树父编码字段 */ + private String treeParentCode; + + /** 树名称字段 */ + private String treeName; + + /** 上级菜单ID字段 */ + private Long parentMenuId; + + /** 上级菜单名称字段 */ + private String parentMenuName; + + public Long getTableId() + { + return tableId; + } + + public void setTableId(Long tableId) + { + this.tableId = tableId; + } + + public String getTableName() + { + return tableName; + } + + public void setTableName(String tableName) + { + this.tableName = tableName; + } + + public String getTableComment() + { + return tableComment; + } + + public void setTableComment(String tableComment) + { + this.tableComment = tableComment; + } + + public String getSubTableName() + { + return subTableName; + } + + public void setSubTableName(String subTableName) + { + this.subTableName = subTableName; + } + + public String getSubTableFkName() + { + return subTableFkName; + } + + public void setSubTableFkName(String subTableFkName) + { + this.subTableFkName = subTableFkName; + } + + public String getClassName() + { + return className; + } + + public void setClassName(String className) + { + this.className = className; + } + + public String getTplCategory() + { + return tplCategory; + } + + public void setTplCategory(String tplCategory) + { + this.tplCategory = tplCategory; + } + + public String getTplWebType() + { + return tplWebType; + } + + public void setTplWebType(String tplWebType) + { + this.tplWebType = tplWebType; + } + + public String getPackageName() + { + return packageName; + } + + public void setPackageName(String packageName) + { + this.packageName = packageName; + } + + public String getModuleName() + { + return moduleName; + } + + public void setModuleName(String moduleName) + { + this.moduleName = moduleName; + } + + public String getBusinessName() + { + return businessName; + } + + public void setBusinessName(String businessName) + { + this.businessName = businessName; + } + + public String getFunctionName() + { + return functionName; + } + + public void setFunctionName(String functionName) + { + this.functionName = functionName; + } + + public String getFunctionAuthor() + { + return functionAuthor; + } + + public void setFunctionAuthor(String functionAuthor) + { + this.functionAuthor = functionAuthor; + } + + public String getGenType() + { + return genType; + } + + public void setGenType(String genType) + { + this.genType = genType; + } + + public String getGenPath() + { + return genPath; + } + + public void setGenPath(String genPath) + { + this.genPath = genPath; + } + + public GenTableColumn getPkColumn() + { + return pkColumn; + } + + public void setPkColumn(GenTableColumn pkColumn) + { + this.pkColumn = pkColumn; + } + + public GenTable getSubTable() + { + return subTable; + } + + public void setSubTable(GenTable subTable) + { + this.subTable = subTable; + } + + public List getColumns() + { + return columns; + } + + public void setColumns(List columns) + { + this.columns = columns; + } + + public String getOptions() + { + return options; + } + + public void setOptions(String options) + { + this.options = options; + } + + public String getTreeCode() + { + return treeCode; + } + + public void setTreeCode(String treeCode) + { + this.treeCode = treeCode; + } + + public String getTreeParentCode() + { + return treeParentCode; + } + + public void setTreeParentCode(String treeParentCode) + { + this.treeParentCode = treeParentCode; + } + + public String getTreeName() + { + return treeName; + } + + public void setTreeName(String treeName) + { + this.treeName = treeName; + } + + public Long getParentMenuId() + { + return parentMenuId; + } + + public void setParentMenuId(Long parentMenuId) + { + this.parentMenuId = parentMenuId; + } + + public String getParentMenuName() + { + return parentMenuName; + } + + public void setParentMenuName(String parentMenuName) + { + this.parentMenuName = parentMenuName; + } + + public boolean isSub() + { + return isSub(this.tplCategory); + } + + public static boolean isSub(String tplCategory) + { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_SUB, tplCategory); + } + + public boolean isTree() + { + return isTree(this.tplCategory); + } + + public static boolean isTree(String tplCategory) + { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_TREE, tplCategory); + } + + public boolean isCrud() + { + return isCrud(this.tplCategory); + } + + public static boolean isCrud(String tplCategory) + { + return tplCategory != null && StringUtils.equals(GenConstants.TPL_CRUD, tplCategory); + } + + public boolean isSuperColumn(String javaField) + { + return isSuperColumn(this.tplCategory, javaField); + } + + public static boolean isSuperColumn(String tplCategory, String javaField) + { + if (isTree(tplCategory)) + { + return StringUtils.equalsAnyIgnoreCase(javaField, + ArrayUtils.addAll(GenConstants.TREE_ENTITY, GenConstants.BASE_ENTITY)); + } + return StringUtils.equalsAnyIgnoreCase(javaField, GenConstants.BASE_ENTITY); + } +} \ No newline at end of file diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTableColumn.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTableColumn.java new file mode 100644 index 0000000..d1733b6 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenTableColumn.java @@ -0,0 +1,373 @@ +package com.ruoyi.generator.domain; + +import javax.validation.constraints.NotBlank; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.utils.StringUtils; + +/** + * 代码生成业务字段表 gen_table_column + * + * @author ruoyi + */ +public class GenTableColumn extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 编号 */ + private Long columnId; + + /** 归属表编号 */ + private Long tableId; + + /** 列名称 */ + private String columnName; + + /** 列描述 */ + private String columnComment; + + /** 列类型 */ + private String columnType; + + /** JAVA类型 */ + private String javaType; + + /** JAVA字段名 */ + @NotBlank(message = "Java属性不能为空") + private String javaField; + + /** 是否主键(1是) */ + private String isPk; + + /** 是否自增(1是) */ + private String isIncrement; + + /** 是否必填(1是) */ + private String isRequired; + + /** 是否为插入字段(1是) */ + private String isInsert; + + /** 是否编辑字段(1是) */ + private String isEdit; + + /** 是否列表字段(1是) */ + private String isList; + + /** 是否查询字段(1是) */ + private String isQuery; + + /** 查询方式(EQ等于、NE不等于、GT大于、LT小于、LIKE模糊、BETWEEN范围) */ + private String queryType; + + /** 显示类型(input文本框、textarea文本域、select下拉框、checkbox复选框、radio单选框、datetime日期控件、image图片上传控件、upload文件上传控件、editor富文本控件) */ + private String htmlType; + + /** 字典类型 */ + private String dictType; + + /** 排序 */ + private Integer sort; + + public void setColumnId(Long columnId) + { + this.columnId = columnId; + } + + public Long getColumnId() + { + return columnId; + } + + public void setTableId(Long tableId) + { + this.tableId = tableId; + } + + public Long getTableId() + { + return tableId; + } + + public void setColumnName(String columnName) + { + this.columnName = columnName; + } + + public String getColumnName() + { + return columnName; + } + + public void setColumnComment(String columnComment) + { + this.columnComment = columnComment; + } + + public String getColumnComment() + { + return columnComment; + } + + public void setColumnType(String columnType) + { + this.columnType = columnType; + } + + public String getColumnType() + { + return columnType; + } + + public void setJavaType(String javaType) + { + this.javaType = javaType; + } + + public String getJavaType() + { + return javaType; + } + + public void setJavaField(String javaField) + { + this.javaField = javaField; + } + + public String getJavaField() + { + return javaField; + } + + public String getCapJavaField() + { + return StringUtils.capitalize(javaField); + } + + public void setIsPk(String isPk) + { + this.isPk = isPk; + } + + public String getIsPk() + { + return isPk; + } + + public boolean isPk() + { + return isPk(this.isPk); + } + + public boolean isPk(String isPk) + { + return isPk != null && StringUtils.equals("1", isPk); + } + + public String getIsIncrement() + { + return isIncrement; + } + + public void setIsIncrement(String isIncrement) + { + this.isIncrement = isIncrement; + } + + public boolean isIncrement() + { + return isIncrement(this.isIncrement); + } + + public boolean isIncrement(String isIncrement) + { + return isIncrement != null && StringUtils.equals("1", isIncrement); + } + + public void setIsRequired(String isRequired) + { + this.isRequired = isRequired; + } + + public String getIsRequired() + { + return isRequired; + } + + public boolean isRequired() + { + return isRequired(this.isRequired); + } + + public boolean isRequired(String isRequired) + { + return isRequired != null && StringUtils.equals("1", isRequired); + } + + public void setIsInsert(String isInsert) + { + this.isInsert = isInsert; + } + + public String getIsInsert() + { + return isInsert; + } + + public boolean isInsert() + { + return isInsert(this.isInsert); + } + + public boolean isInsert(String isInsert) + { + return isInsert != null && StringUtils.equals("1", isInsert); + } + + public void setIsEdit(String isEdit) + { + this.isEdit = isEdit; + } + + public String getIsEdit() + { + return isEdit; + } + + public boolean isEdit() + { + return isInsert(this.isEdit); + } + + public boolean isEdit(String isEdit) + { + return isEdit != null && StringUtils.equals("1", isEdit); + } + + public void setIsList(String isList) + { + this.isList = isList; + } + + public String getIsList() + { + return isList; + } + + public boolean isList() + { + return isList(this.isList); + } + + public boolean isList(String isList) + { + return isList != null && StringUtils.equals("1", isList); + } + + public void setIsQuery(String isQuery) + { + this.isQuery = isQuery; + } + + public String getIsQuery() + { + return isQuery; + } + + public boolean isQuery() + { + return isQuery(this.isQuery); + } + + public boolean isQuery(String isQuery) + { + return isQuery != null && StringUtils.equals("1", isQuery); + } + + public void setQueryType(String queryType) + { + this.queryType = queryType; + } + + public String getQueryType() + { + return queryType; + } + + public String getHtmlType() + { + return htmlType; + } + + public void setHtmlType(String htmlType) + { + this.htmlType = htmlType; + } + + public void setDictType(String dictType) + { + this.dictType = dictType; + } + + public String getDictType() + { + return dictType; + } + + public void setSort(Integer sort) + { + this.sort = sort; + } + + public Integer getSort() + { + return sort; + } + + public boolean isSuperColumn() + { + return isSuperColumn(this.javaField); + } + + public static boolean isSuperColumn(String javaField) + { + return StringUtils.equalsAnyIgnoreCase(javaField, + // BaseEntity + "createBy", "createTime", "updateBy", "updateTime", "remark", + // TreeEntity + "parentName", "parentId", "orderNum", "ancestors"); + } + + public boolean isUsableColumn() + { + return isUsableColumn(javaField); + } + + public static boolean isUsableColumn(String javaField) + { + // isSuperColumn()中的名单用于避免生成多余Domain属性,若某些属性在生成页面时需要用到不能忽略,则放在此处白名单 + return StringUtils.equalsAnyIgnoreCase(javaField, "parentId", "orderNum", "remark"); + } + + public String readConverterExp() + { + String remarks = StringUtils.substringBetween(this.columnComment, "(", ")"); + StringBuffer sb = new StringBuffer(); + if (StringUtils.isNotEmpty(remarks)) + { + for (String value : remarks.split(" ")) + { + if (StringUtils.isNotEmpty(value)) + { + Object startStr = value.subSequence(0, 1); + String endStr = value.substring(1); + sb.append("").append(startStr).append("=").append(endStr).append(","); + } + } + return sb.deleteCharAt(sb.length() - 1).toString(); + } + else + { + return this.columnComment; + } + } +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableColumnMapper.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableColumnMapper.java new file mode 100644 index 0000000..951e166 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableColumnMapper.java @@ -0,0 +1,60 @@ +package com.ruoyi.generator.mapper; + +import java.util.List; +import com.ruoyi.generator.domain.GenTableColumn; + +/** + * 业务字段 数据层 + * + * @author ruoyi + */ +public interface GenTableColumnMapper +{ + /** + * 根据表名称查询列信息 + * + * @param tableName 表名称 + * @return 列信息 + */ + public List selectDbTableColumnsByName(String tableName); + + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + public List selectGenTableColumnListByTableId(Long tableId); + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int insertGenTableColumn(GenTableColumn genTableColumn); + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int updateGenTableColumn(GenTableColumn genTableColumn); + + /** + * 删除业务字段 + * + * @param genTableColumns 列数据 + * @return 结果 + */ + public int deleteGenTableColumns(List genTableColumns); + + /** + * 批量删除业务字段 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteGenTableColumnByIds(Long[] ids); +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableMapper.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableMapper.java new file mode 100644 index 0000000..937656d --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/GenTableMapper.java @@ -0,0 +1,91 @@ +package com.ruoyi.generator.mapper; + +import java.util.List; +import com.ruoyi.generator.domain.GenTable; + +/** + * 业务 数据层 + * + * @author ruoyi + */ +public interface GenTableMapper +{ + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + public List selectGenTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + public List selectDbTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + public List selectDbTableListByNames(String[] tableNames); + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + public List selectGenTableAll(); + + /** + * 查询表ID业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + public GenTable selectGenTableById(Long id); + + /** + * 查询表名称业务信息 + * + * @param tableName 表名称 + * @return 业务信息 + */ + public GenTable selectGenTableByName(String tableName); + + /** + * 新增业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + public int insertGenTable(GenTable genTable); + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + public int updateGenTable(GenTable genTable); + + /** + * 批量删除业务 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteGenTableByIds(Long[] ids); + + /** + * 创建表 + * + * @param sql 表结构 + * @return 结果 + */ + public int createTable(String sql); +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableColumnServiceImpl.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableColumnServiceImpl.java new file mode 100644 index 0000000..0679689 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableColumnServiceImpl.java @@ -0,0 +1,68 @@ +package com.ruoyi.generator.service; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.generator.domain.GenTableColumn; +import com.ruoyi.generator.mapper.GenTableColumnMapper; + +/** + * 业务字段 服务层实现 + * + * @author ruoyi + */ +@Service +public class GenTableColumnServiceImpl implements IGenTableColumnService +{ + @Autowired + private GenTableColumnMapper genTableColumnMapper; + + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + @Override + public List selectGenTableColumnListByTableId(Long tableId) + { + return genTableColumnMapper.selectGenTableColumnListByTableId(tableId); + } + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + @Override + public int insertGenTableColumn(GenTableColumn genTableColumn) + { + return genTableColumnMapper.insertGenTableColumn(genTableColumn); + } + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + @Override + public int updateGenTableColumn(GenTableColumn genTableColumn) + { + return genTableColumnMapper.updateGenTableColumn(genTableColumn); + } + + /** + * 删除业务字段对象 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + @Override + public int deleteGenTableColumnByIds(String ids) + { + return genTableColumnMapper.deleteGenTableColumnByIds(Convert.toLongArray(ids)); + } +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java new file mode 100644 index 0000000..fe4312f --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java @@ -0,0 +1,531 @@ +package com.ruoyi.generator.service; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.velocity.Template; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.Velocity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.GenConstants; +import com.ruoyi.common.core.text.CharsetKit; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.generator.domain.GenTable; +import com.ruoyi.generator.domain.GenTableColumn; +import com.ruoyi.generator.mapper.GenTableColumnMapper; +import com.ruoyi.generator.mapper.GenTableMapper; +import com.ruoyi.generator.util.GenUtils; +import com.ruoyi.generator.util.VelocityInitializer; +import com.ruoyi.generator.util.VelocityUtils; + +/** + * 业务 服务层实现 + * + * @author ruoyi + */ +@Service +public class GenTableServiceImpl implements IGenTableService +{ + private static final Logger log = LoggerFactory.getLogger(GenTableServiceImpl.class); + + @Autowired + private GenTableMapper genTableMapper; + + @Autowired + private GenTableColumnMapper genTableColumnMapper; + + /** + * 查询业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + @Override + public GenTable selectGenTableById(Long id) + { + GenTable genTable = genTableMapper.selectGenTableById(id); + setTableFromOptions(genTable); + return genTable; + } + + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + @Override + public List selectGenTableList(GenTable genTable) + { + return genTableMapper.selectGenTableList(genTable); + } + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + @Override + public List selectDbTableList(GenTable genTable) + { + return genTableMapper.selectDbTableList(genTable); + } + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + @Override + public List selectDbTableListByNames(String[] tableNames) + { + return genTableMapper.selectDbTableListByNames(tableNames); + } + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + @Override + public List selectGenTableAll() + { + return genTableMapper.selectGenTableAll(); + } + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + @Override + @Transactional + public void updateGenTable(GenTable genTable) + { + String options = JSON.toJSONString(genTable.getParams()); + genTable.setOptions(options); + int row = genTableMapper.updateGenTable(genTable); + if (row > 0) + { + for (GenTableColumn genTableColumn : genTable.getColumns()) + { + genTableColumnMapper.updateGenTableColumn(genTableColumn); + } + } + } + + /** + * 删除业务对象 + * + * @param tableIds 需要删除的数据ID + * @return 结果 + */ + @Override + @Transactional + public void deleteGenTableByIds(Long[] tableIds) + { + genTableMapper.deleteGenTableByIds(tableIds); + genTableColumnMapper.deleteGenTableColumnByIds(tableIds); + } + + /** + * 创建表 + * + * @param sql 创建表语句 + * @return 结果 + */ + @Override + public boolean createTable(String sql) + { + return genTableMapper.createTable(sql) == 0; + } + + /** + * 导入表结构 + * + * @param tableList 导入表列表 + */ + @Override + @Transactional + public void importGenTable(List tableList, String operName) + { + try + { + for (GenTable table : tableList) + { + String tableName = table.getTableName(); + GenUtils.initTable(table, operName); + int row = genTableMapper.insertGenTable(table); + if (row > 0) + { + // 保存列信息 + List genTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName); + for (GenTableColumn column : genTableColumns) + { + GenUtils.initColumnField(column, table); + genTableColumnMapper.insertGenTableColumn(column); + } + } + } + } + catch (Exception e) + { + throw new ServiceException("导入失败:" + e.getMessage()); + } + } + + /** + * 预览代码 + * + * @param tableId 表编号 + * @return 预览数据列表 + */ + @Override + public Map previewCode(Long tableId) + { + Map dataMap = new LinkedHashMap<>(); + // 查询表信息 + GenTable table = genTableMapper.selectGenTableById(tableId); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table.getTplCategory(), table.getTplWebType()); + for (String template : templates) + { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + dataMap.put(template, sw.toString()); + } + return dataMap; + } + + /** + * 生成代码(下载方式) + * + * @param tableName 表名称 + * @return 数据 + */ + @Override + public byte[] downloadCode(String tableName) + { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream); + generatorCode(tableName, zip); + IOUtils.closeQuietly(zip); + return outputStream.toByteArray(); + } + + /** + * 生成代码(自定义路径) + * + * @param tableName 表名称 + */ + @Override + public void generatorCode(String tableName) + { + // 查询表信息 + GenTable table = genTableMapper.selectGenTableByName(tableName); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table.getTplCategory(), table.getTplWebType()); + for (String template : templates) + { + if (!StringUtils.containsAny(template, "sql.vm", "api.js.vm", "index.vue.vm", "index-tree.vue.vm")) + { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + try + { + String path = getGenPath(table, template); + FileUtils.writeStringToFile(new File(path), sw.toString(), CharsetKit.UTF_8); + } + catch (IOException e) + { + throw new ServiceException("渲染模板失败,表名:" + table.getTableName()); + } + } + } + } + + /** + * 同步数据库 + * + * @param tableName 表名称 + */ + @Override + @Transactional + public void synchDb(String tableName) + { + GenTable table = genTableMapper.selectGenTableByName(tableName); + List tableColumns = table.getColumns(); + Map tableColumnMap = tableColumns.stream().collect(Collectors.toMap(GenTableColumn::getColumnName, Function.identity())); + + List dbTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName); + if (StringUtils.isEmpty(dbTableColumns)) + { + throw new ServiceException("同步数据失败,原表结构不存在"); + } + List dbTableColumnNames = dbTableColumns.stream().map(GenTableColumn::getColumnName).collect(Collectors.toList()); + + dbTableColumns.forEach(column -> { + GenUtils.initColumnField(column, table); + if (tableColumnMap.containsKey(column.getColumnName())) + { + GenTableColumn prevColumn = tableColumnMap.get(column.getColumnName()); + column.setColumnId(prevColumn.getColumnId()); + if (column.isList()) + { + // 如果是列表,继续保留查询方式/字典类型选项 + column.setDictType(prevColumn.getDictType()); + column.setQueryType(prevColumn.getQueryType()); + } + if (StringUtils.isNotEmpty(prevColumn.getIsRequired()) && !column.isPk() + && (column.isInsert() || column.isEdit()) + && ((column.isUsableColumn()) || (!column.isSuperColumn()))) + { + // 如果是(新增/修改&非主键/非忽略及父属性),继续保留必填/显示类型选项 + column.setIsRequired(prevColumn.getIsRequired()); + column.setHtmlType(prevColumn.getHtmlType()); + } + genTableColumnMapper.updateGenTableColumn(column); + } + else + { + genTableColumnMapper.insertGenTableColumn(column); + } + }); + + List delColumns = tableColumns.stream().filter(column -> !dbTableColumnNames.contains(column.getColumnName())).collect(Collectors.toList()); + if (StringUtils.isNotEmpty(delColumns)) + { + genTableColumnMapper.deleteGenTableColumns(delColumns); + } + } + + /** + * 批量生成代码(下载方式) + * + * @param tableNames 表数组 + * @return 数据 + */ + @Override + public byte[] downloadCode(String[] tableNames) + { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream); + for (String tableName : tableNames) + { + generatorCode(tableName, zip); + } + IOUtils.closeQuietly(zip); + return outputStream.toByteArray(); + } + + /** + * 查询表信息并生成代码 + */ + private void generatorCode(String tableName, ZipOutputStream zip) + { + // 查询表信息 + GenTable table = genTableMapper.selectGenTableByName(tableName); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table.getTplCategory(), table.getTplWebType()); + for (String template : templates) + { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + try + { + // 添加到zip + zip.putNextEntry(new ZipEntry(VelocityUtils.getFileName(template, table))); + IOUtils.write(sw.toString(), zip, Constants.UTF8); + IOUtils.closeQuietly(sw); + zip.flush(); + zip.closeEntry(); + } + catch (IOException e) + { + log.error("渲染模板失败,表名:" + table.getTableName(), e); + } + } + } + + /** + * 修改保存参数校验 + * + * @param genTable 业务信息 + */ + @Override + public void validateEdit(GenTable genTable) + { + if (GenConstants.TPL_TREE.equals(genTable.getTplCategory())) + { + String options = JSON.toJSONString(genTable.getParams()); + JSONObject paramsObj = JSON.parseObject(options); + if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_CODE))) + { + throw new ServiceException("树编码字段不能为空"); + } + else if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_PARENT_CODE))) + { + throw new ServiceException("树父编码字段不能为空"); + } + else if (StringUtils.isEmpty(paramsObj.getString(GenConstants.TREE_NAME))) + { + throw new ServiceException("树名称字段不能为空"); + } + } + else if (GenConstants.TPL_SUB.equals(genTable.getTplCategory())) + { + if (StringUtils.isEmpty(genTable.getSubTableName())) + { + throw new ServiceException("关联子表的表名不能为空"); + } + else if (StringUtils.isEmpty(genTable.getSubTableFkName())) + { + throw new ServiceException("子表关联的外键名不能为空"); + } + } + } + + /** + * 设置主键列信息 + * + * @param table 业务表信息 + */ + public void setPkColumn(GenTable table) + { + for (GenTableColumn column : table.getColumns()) + { + if (column.isPk()) + { + table.setPkColumn(column); + break; + } + } + if (StringUtils.isNull(table.getPkColumn())) + { + table.setPkColumn(table.getColumns().get(0)); + } + if (GenConstants.TPL_SUB.equals(table.getTplCategory())) + { + for (GenTableColumn column : table.getSubTable().getColumns()) + { + if (column.isPk()) + { + table.getSubTable().setPkColumn(column); + break; + } + } + if (StringUtils.isNull(table.getSubTable().getPkColumn())) + { + table.getSubTable().setPkColumn(table.getSubTable().getColumns().get(0)); + } + } + } + + /** + * 设置主子表信息 + * + * @param table 业务表信息 + */ + public void setSubTable(GenTable table) + { + String subTableName = table.getSubTableName(); + if (StringUtils.isNotEmpty(subTableName)) + { + table.setSubTable(genTableMapper.selectGenTableByName(subTableName)); + } + } + + /** + * 设置代码生成其他选项值 + * + * @param genTable 设置后的生成对象 + */ + public void setTableFromOptions(GenTable genTable) + { + JSONObject paramsObj = JSON.parseObject(genTable.getOptions()); + if (StringUtils.isNotNull(paramsObj)) + { + String treeCode = paramsObj.getString(GenConstants.TREE_CODE); + String treeParentCode = paramsObj.getString(GenConstants.TREE_PARENT_CODE); + String treeName = paramsObj.getString(GenConstants.TREE_NAME); + Long parentMenuId = paramsObj.getLongValue(GenConstants.PARENT_MENU_ID); + String parentMenuName = paramsObj.getString(GenConstants.PARENT_MENU_NAME); + + genTable.setTreeCode(treeCode); + genTable.setTreeParentCode(treeParentCode); + genTable.setTreeName(treeName); + genTable.setParentMenuId(parentMenuId); + genTable.setParentMenuName(parentMenuName); + } + } + + /** + * 获取代码生成地址 + * + * @param table 业务表信息 + * @param template 模板文件路径 + * @return 生成地址 + */ + public static String getGenPath(GenTable table, String template) + { + String genPath = table.getGenPath(); + if (StringUtils.equals(genPath, "/")) + { + return System.getProperty("user.dir") + File.separator + "src" + File.separator + VelocityUtils.getFileName(template, table); + } + return genPath + File.separator + VelocityUtils.getFileName(template, table); + } +} \ No newline at end of file diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableColumnService.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableColumnService.java new file mode 100644 index 0000000..3037f70 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableColumnService.java @@ -0,0 +1,44 @@ +package com.ruoyi.generator.service; + +import java.util.List; +import com.ruoyi.generator.domain.GenTableColumn; + +/** + * 业务字段 服务层 + * + * @author ruoyi + */ +public interface IGenTableColumnService +{ + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + public List selectGenTableColumnListByTableId(Long tableId); + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int insertGenTableColumn(GenTableColumn genTableColumn); + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int updateGenTableColumn(GenTableColumn genTableColumn); + + /** + * 删除业务字段信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteGenTableColumnByIds(String ids); +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableService.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableService.java new file mode 100644 index 0000000..695426e --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/service/IGenTableService.java @@ -0,0 +1,130 @@ +package com.ruoyi.generator.service; + +import java.util.List; +import java.util.Map; +import com.ruoyi.generator.domain.GenTable; + +/** + * 业务 服务层 + * + * @author ruoyi + */ +public interface IGenTableService +{ + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + public List selectGenTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + public List selectDbTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + public List selectDbTableListByNames(String[] tableNames); + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + public List selectGenTableAll(); + + /** + * 查询业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + public GenTable selectGenTableById(Long id); + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + public void updateGenTable(GenTable genTable); + + /** + * 删除业务信息 + * + * @param tableIds 需要删除的表数据ID + * @return 结果 + */ + public void deleteGenTableByIds(Long[] tableIds); + + /** + * 创建表 + * + * @param sql 创建表语句 + * @return 结果 + */ + public boolean createTable(String sql); + + /** + * 导入表结构 + * + * @param tableList 导入表列表 + * @param operName 操作人员 + */ + public void importGenTable(List tableList, String operName); + + /** + * 预览代码 + * + * @param tableId 表编号 + * @return 预览数据列表 + */ + public Map previewCode(Long tableId); + + /** + * 生成代码(下载方式) + * + * @param tableName 表名称 + * @return 数据 + */ + public byte[] downloadCode(String tableName); + + /** + * 生成代码(自定义路径) + * + * @param tableName 表名称 + * @return 数据 + */ + public void generatorCode(String tableName); + + /** + * 同步数据库 + * + * @param tableName 表名称 + */ + public void synchDb(String tableName); + + /** + * 批量生成代码(下载方式) + * + * @param tableNames 表数组 + * @return 数据 + */ + public byte[] downloadCode(String[] tableNames); + + /** + * 修改保存参数校验 + * + * @param genTable 业务信息 + */ + public void validateEdit(GenTable genTable); +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/util/GenUtils.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/GenUtils.java new file mode 100644 index 0000000..e7ebc20 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/GenUtils.java @@ -0,0 +1,257 @@ +package com.ruoyi.generator.util; + +import java.util.Arrays; +import org.apache.commons.lang3.RegExUtils; +import com.ruoyi.common.constant.GenConstants; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.generator.config.GenConfig; +import com.ruoyi.generator.domain.GenTable; +import com.ruoyi.generator.domain.GenTableColumn; + +/** + * 代码生成器 工具类 + * + * @author ruoyi + */ +public class GenUtils +{ + /** + * 初始化表信息 + */ + public static void initTable(GenTable genTable, String operName) + { + genTable.setClassName(convertClassName(genTable.getTableName())); + genTable.setPackageName(GenConfig.getPackageName()); + genTable.setModuleName(getModuleName(GenConfig.getPackageName())); + genTable.setBusinessName(getBusinessName(genTable.getTableName())); + genTable.setFunctionName(replaceText(genTable.getTableComment())); + genTable.setFunctionAuthor(GenConfig.getAuthor()); + genTable.setCreateBy(operName); + } + + /** + * 初始化列属性字段 + */ + public static void initColumnField(GenTableColumn column, GenTable table) + { + String dataType = getDbType(column.getColumnType()); + String columnName = column.getColumnName(); + column.setTableId(table.getTableId()); + column.setCreateBy(table.getCreateBy()); + // 设置java字段名 + column.setJavaField(StringUtils.toCamelCase(columnName)); + // 设置默认类型 + column.setJavaType(GenConstants.TYPE_STRING); + column.setQueryType(GenConstants.QUERY_EQ); + + if (arraysContains(GenConstants.COLUMNTYPE_STR, dataType) || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType)) + { + // 字符串长度超过500设置为文本域 + Integer columnLength = getColumnLength(column.getColumnType()); + String htmlType = columnLength >= 500 || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType) ? GenConstants.HTML_TEXTAREA : GenConstants.HTML_INPUT; + column.setHtmlType(htmlType); + } + else if (arraysContains(GenConstants.COLUMNTYPE_TIME, dataType)) + { + column.setJavaType(GenConstants.TYPE_DATE); + column.setHtmlType(GenConstants.HTML_DATETIME); + } + else if (arraysContains(GenConstants.COLUMNTYPE_NUMBER, dataType)) + { + column.setHtmlType(GenConstants.HTML_INPUT); + + // 如果是浮点型 统一用BigDecimal + String[] str = StringUtils.split(StringUtils.substringBetween(column.getColumnType(), "(", ")"), ","); + if (str != null && str.length == 2 && Integer.parseInt(str[1]) > 0) + { + column.setJavaType(GenConstants.TYPE_BIGDECIMAL); + } + // 如果是整形 + else if (str != null && str.length == 1 && Integer.parseInt(str[0]) <= 10) + { + column.setJavaType(GenConstants.TYPE_INTEGER); + } + // 长整形 + else + { + column.setJavaType(GenConstants.TYPE_LONG); + } + } + + // 插入字段(默认所有字段都需要插入) + column.setIsInsert(GenConstants.REQUIRE); + + // 编辑字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_EDIT, columnName) && !column.isPk()) + { + column.setIsEdit(GenConstants.REQUIRE); + } + // 列表字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_LIST, columnName) && !column.isPk()) + { + column.setIsList(GenConstants.REQUIRE); + } + // 查询字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_QUERY, columnName) && !column.isPk()) + { + column.setIsQuery(GenConstants.REQUIRE); + } + + // 查询字段类型 + if (StringUtils.endsWithIgnoreCase(columnName, "name")) + { + column.setQueryType(GenConstants.QUERY_LIKE); + } + // 状态字段设置单选框 + if (StringUtils.endsWithIgnoreCase(columnName, "status")) + { + column.setHtmlType(GenConstants.HTML_RADIO); + } + // 类型&性别字段设置下拉框 + else if (StringUtils.endsWithIgnoreCase(columnName, "type") + || StringUtils.endsWithIgnoreCase(columnName, "sex")) + { + column.setHtmlType(GenConstants.HTML_SELECT); + } + // 图片字段设置图片上传控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "image")) + { + column.setHtmlType(GenConstants.HTML_IMAGE_UPLOAD); + } + // 文件字段设置文件上传控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "file")) + { + column.setHtmlType(GenConstants.HTML_FILE_UPLOAD); + } + // 内容字段设置富文本控件 + else if (StringUtils.endsWithIgnoreCase(columnName, "content")) + { + column.setHtmlType(GenConstants.HTML_EDITOR); + } + } + + /** + * 校验数组是否包含指定值 + * + * @param arr 数组 + * @param targetValue 值 + * @return 是否包含 + */ + public static boolean arraysContains(String[] arr, String targetValue) + { + return Arrays.asList(arr).contains(targetValue); + } + + /** + * 获取模块名 + * + * @param packageName 包名 + * @return 模块名 + */ + public static String getModuleName(String packageName) + { + int lastIndex = packageName.lastIndexOf("."); + int nameLength = packageName.length(); + return StringUtils.substring(packageName, lastIndex + 1, nameLength); + } + + /** + * 获取业务名 + * + * @param tableName 表名 + * @return 业务名 + */ + public static String getBusinessName(String tableName) + { + int lastIndex = tableName.lastIndexOf("_"); + int nameLength = tableName.length(); + return StringUtils.substring(tableName, lastIndex + 1, nameLength); + } + + /** + * 表名转换成Java类名 + * + * @param tableName 表名称 + * @return 类名 + */ + public static String convertClassName(String tableName) + { + boolean autoRemovePre = GenConfig.getAutoRemovePre(); + String tablePrefix = GenConfig.getTablePrefix(); + if (autoRemovePre && StringUtils.isNotEmpty(tablePrefix)) + { + String[] searchList = StringUtils.split(tablePrefix, ","); + tableName = replaceFirst(tableName, searchList); + } + return StringUtils.convertToCamelCase(tableName); + } + + /** + * 批量替换前缀 + * + * @param replacementm 替换值 + * @param searchList 替换列表 + * @return + */ + public static String replaceFirst(String replacementm, String[] searchList) + { + String text = replacementm; + for (String searchString : searchList) + { + if (replacementm.startsWith(searchString)) + { + text = replacementm.replaceFirst(searchString, ""); + break; + } + } + return text; + } + + /** + * 关键字替换 + * + * @param text 需要被替换的名字 + * @return 替换后的名字 + */ + public static String replaceText(String text) + { + return RegExUtils.replaceAll(text, "(?:表|若依)", ""); + } + + /** + * 获取数据库类型字段 + * + * @param columnType 列类型 + * @return 截取后的列类型 + */ + public static String getDbType(String columnType) + { + if (StringUtils.indexOf(columnType, "(") > 0) + { + return StringUtils.substringBefore(columnType, "("); + } + else + { + return columnType; + } + } + + /** + * 获取字段长度 + * + * @param columnType 列类型 + * @return 截取后的列类型 + */ + public static Integer getColumnLength(String columnType) + { + if (StringUtils.indexOf(columnType, "(") > 0) + { + String length = StringUtils.substringBetween(columnType, "(", ")"); + return Integer.valueOf(length); + } + else + { + return 0; + } + } +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityInitializer.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityInitializer.java new file mode 100644 index 0000000..9f69403 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityInitializer.java @@ -0,0 +1,34 @@ +package com.ruoyi.generator.util; + +import java.util.Properties; +import org.apache.velocity.app.Velocity; +import com.ruoyi.common.constant.Constants; + +/** + * VelocityEngine工厂 + * + * @author ruoyi + */ +public class VelocityInitializer +{ + /** + * 初始化vm方法 + */ + public static void initVelocity() + { + Properties p = new Properties(); + try + { + // 加载classpath目录下的vm文件 + p.setProperty("resource.loader.file.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader"); + // 定义字符集 + p.setProperty(Velocity.INPUT_ENCODING, Constants.UTF8); + // 初始化Velocity引擎,指定配置Properties + Velocity.init(p); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } +} diff --git a/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java new file mode 100644 index 0000000..1a14681 --- /dev/null +++ b/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java @@ -0,0 +1,408 @@ +package com.ruoyi.generator.util; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.apache.velocity.VelocityContext; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.common.constant.GenConstants; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.generator.domain.GenTable; +import com.ruoyi.generator.domain.GenTableColumn; + +/** + * 模板处理工具类 + * + * @author ruoyi + */ +public class VelocityUtils +{ + /** 项目空间路径 */ + private static final String PROJECT_PATH = "main/java"; + + /** mybatis空间路径 */ + private static final String MYBATIS_PATH = "main/resources/mapper"; + + /** 默认上级菜单,系统工具 */ + private static final String DEFAULT_PARENT_MENU_ID = "3"; + + /** + * 设置模板变量信息 + * + * @return 模板列表 + */ + public static VelocityContext prepareContext(GenTable genTable) + { + String moduleName = genTable.getModuleName(); + String businessName = genTable.getBusinessName(); + String packageName = genTable.getPackageName(); + String tplCategory = genTable.getTplCategory(); + String functionName = genTable.getFunctionName(); + + VelocityContext velocityContext = new VelocityContext(); + velocityContext.put("tplCategory", genTable.getTplCategory()); + velocityContext.put("tableName", genTable.getTableName()); + velocityContext.put("functionName", StringUtils.isNotEmpty(functionName) ? functionName : "【请填写功能名称】"); + velocityContext.put("ClassName", genTable.getClassName()); + velocityContext.put("className", StringUtils.uncapitalize(genTable.getClassName())); + velocityContext.put("moduleName", genTable.getModuleName()); + velocityContext.put("BusinessName", StringUtils.capitalize(genTable.getBusinessName())); + velocityContext.put("businessName", genTable.getBusinessName()); + velocityContext.put("basePackage", getPackagePrefix(packageName)); + velocityContext.put("packageName", packageName); + velocityContext.put("author", genTable.getFunctionAuthor()); + velocityContext.put("datetime", DateUtils.getDate()); + velocityContext.put("pkColumn", genTable.getPkColumn()); + velocityContext.put("importList", getImportList(genTable)); + velocityContext.put("permissionPrefix", getPermissionPrefix(moduleName, businessName)); + velocityContext.put("columns", genTable.getColumns()); + velocityContext.put("table", genTable); + velocityContext.put("dicts", getDicts(genTable)); + setMenuVelocityContext(velocityContext, genTable); + if (GenConstants.TPL_TREE.equals(tplCategory)) + { + setTreeVelocityContext(velocityContext, genTable); + } + if (GenConstants.TPL_SUB.equals(tplCategory)) + { + setSubVelocityContext(velocityContext, genTable); + } + return velocityContext; + } + + public static void setMenuVelocityContext(VelocityContext context, GenTable genTable) + { + String options = genTable.getOptions(); + JSONObject paramsObj = JSON.parseObject(options); + String parentMenuId = getParentMenuId(paramsObj); + context.put("parentMenuId", parentMenuId); + } + + public static void setTreeVelocityContext(VelocityContext context, GenTable genTable) + { + String options = genTable.getOptions(); + JSONObject paramsObj = JSON.parseObject(options); + String treeCode = getTreecode(paramsObj); + String treeParentCode = getTreeParentCode(paramsObj); + String treeName = getTreeName(paramsObj); + + context.put("treeCode", treeCode); + context.put("treeParentCode", treeParentCode); + context.put("treeName", treeName); + context.put("expandColumn", getExpandColumn(genTable)); + if (paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) + { + context.put("tree_parent_code", paramsObj.getString(GenConstants.TREE_PARENT_CODE)); + } + if (paramsObj.containsKey(GenConstants.TREE_NAME)) + { + context.put("tree_name", paramsObj.getString(GenConstants.TREE_NAME)); + } + } + + public static void setSubVelocityContext(VelocityContext context, GenTable genTable) + { + GenTable subTable = genTable.getSubTable(); + String subTableName = genTable.getSubTableName(); + String subTableFkName = genTable.getSubTableFkName(); + String subClassName = genTable.getSubTable().getClassName(); + String subTableFkClassName = StringUtils.convertToCamelCase(subTableFkName); + + context.put("subTable", subTable); + context.put("subTableName", subTableName); + context.put("subTableFkName", subTableFkName); + context.put("subTableFkClassName", subTableFkClassName); + context.put("subTableFkclassName", StringUtils.uncapitalize(subTableFkClassName)); + context.put("subClassName", subClassName); + context.put("subclassName", StringUtils.uncapitalize(subClassName)); + context.put("subImportList", getImportList(genTable.getSubTable())); + } + + /** + * 获取模板信息 + * @param tplCategory 生成的模板 + * @param tplWebType 前端类型 + * @return 模板列表 + */ + public static List getTemplateList(String tplCategory, String tplWebType) + { + String useWebType = "vm/vue"; + if ("element-plus".equals(tplWebType)) + { + useWebType = "vm/vue/v3"; + } + List templates = new ArrayList(); + templates.add("vm/java/domain.java.vm"); + templates.add("vm/java/mapper.java.vm"); + templates.add("vm/java/service.java.vm"); + templates.add("vm/java/serviceImpl.java.vm"); + templates.add("vm/java/controller.java.vm"); + templates.add("vm/xml/mapper.xml.vm"); + templates.add("vm/sql/sql.vm"); + templates.add("vm/js/api.js.vm"); + if (GenConstants.TPL_CRUD.equals(tplCategory)) + { + templates.add(useWebType + "/index.vue.vm"); + } + else if (GenConstants.TPL_TREE.equals(tplCategory)) + { + templates.add(useWebType + "/index-tree.vue.vm"); + } + else if (GenConstants.TPL_SUB.equals(tplCategory)) + { + templates.add(useWebType + "/index.vue.vm"); + templates.add("vm/java/sub-domain.java.vm"); + } + return templates; + } + + /** + * 获取文件名 + */ + public static String getFileName(String template, GenTable genTable) + { + // 文件名称 + String fileName = ""; + // 包路径 + String packageName = genTable.getPackageName(); + // 模块名 + String moduleName = genTable.getModuleName(); + // 大写类名 + String className = genTable.getClassName(); + // 业务名称 + String businessName = genTable.getBusinessName(); + + String javaPath = PROJECT_PATH + "/" + StringUtils.replace(packageName, ".", "/"); + String mybatisPath = MYBATIS_PATH + "/" + moduleName; + String vuePath = "vue"; + + if (template.contains("domain.java.vm")) + { + fileName = StringUtils.format("{}/domain/{}.java", javaPath, className); + } + if (template.contains("sub-domain.java.vm") && StringUtils.equals(GenConstants.TPL_SUB, genTable.getTplCategory())) + { + fileName = StringUtils.format("{}/domain/{}.java", javaPath, genTable.getSubTable().getClassName()); + } + else if (template.contains("mapper.java.vm")) + { + fileName = StringUtils.format("{}/mapper/{}Mapper.java", javaPath, className); + } + else if (template.contains("service.java.vm")) + { + fileName = StringUtils.format("{}/service/I{}Service.java", javaPath, className); + } + else if (template.contains("serviceImpl.java.vm")) + { + fileName = StringUtils.format("{}/service/impl/{}ServiceImpl.java", javaPath, className); + } + else if (template.contains("controller.java.vm")) + { + fileName = StringUtils.format("{}/controller/{}Controller.java", javaPath, className); + } + else if (template.contains("mapper.xml.vm")) + { + fileName = StringUtils.format("{}/{}Mapper.xml", mybatisPath, className); + } + else if (template.contains("sql.vm")) + { + fileName = businessName + "Menu.sql"; + } + else if (template.contains("api.js.vm")) + { + fileName = StringUtils.format("{}/api/{}/{}.js", vuePath, moduleName, businessName); + } + else if (template.contains("index.vue.vm")) + { + fileName = StringUtils.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName); + } + else if (template.contains("index-tree.vue.vm")) + { + fileName = StringUtils.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName); + } + return fileName; + } + + /** + * 获取包前缀 + * + * @param packageName 包名称 + * @return 包前缀名称 + */ + public static String getPackagePrefix(String packageName) + { + int lastIndex = packageName.lastIndexOf("."); + return StringUtils.substring(packageName, 0, lastIndex); + } + + /** + * 根据列类型获取导入包 + * + * @param genTable 业务表对象 + * @return 返回需要导入的包列表 + */ + public static HashSet getImportList(GenTable genTable) + { + List columns = genTable.getColumns(); + GenTable subGenTable = genTable.getSubTable(); + HashSet importList = new HashSet(); + if (StringUtils.isNotNull(subGenTable)) + { + importList.add("java.util.List"); + } + for (GenTableColumn column : columns) + { + if (!column.isSuperColumn() && GenConstants.TYPE_DATE.equals(column.getJavaType())) + { + importList.add("java.util.Date"); + importList.add("com.fasterxml.jackson.annotation.JsonFormat"); + } + else if (!column.isSuperColumn() && GenConstants.TYPE_BIGDECIMAL.equals(column.getJavaType())) + { + importList.add("java.math.BigDecimal"); + } + } + return importList; + } + + /** + * 根据列类型获取字典组 + * + * @param genTable 业务表对象 + * @return 返回字典组 + */ + public static String getDicts(GenTable genTable) + { + List columns = genTable.getColumns(); + Set dicts = new HashSet(); + addDicts(dicts, columns); + if (StringUtils.isNotNull(genTable.getSubTable())) + { + List subColumns = genTable.getSubTable().getColumns(); + addDicts(dicts, subColumns); + } + return StringUtils.join(dicts, ", "); + } + + /** + * 添加字典列表 + * + * @param dicts 字典列表 + * @param columns 列集合 + */ + public static void addDicts(Set dicts, List columns) + { + for (GenTableColumn column : columns) + { + if (!column.isSuperColumn() && StringUtils.isNotEmpty(column.getDictType()) && StringUtils.equalsAny( + column.getHtmlType(), + new String[] { GenConstants.HTML_SELECT, GenConstants.HTML_RADIO, GenConstants.HTML_CHECKBOX })) + { + dicts.add("'" + column.getDictType() + "'"); + } + } + } + + /** + * 获取权限前缀 + * + * @param moduleName 模块名称 + * @param businessName 业务名称 + * @return 返回权限前缀 + */ + public static String getPermissionPrefix(String moduleName, String businessName) + { + return StringUtils.format("{}:{}", moduleName, businessName); + } + + /** + * 获取上级菜单ID字段 + * + * @param paramsObj 生成其他选项 + * @return 上级菜单ID字段 + */ + public static String getParentMenuId(JSONObject paramsObj) + { + if (StringUtils.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.PARENT_MENU_ID) + && StringUtils.isNotEmpty(paramsObj.getString(GenConstants.PARENT_MENU_ID))) + { + return paramsObj.getString(GenConstants.PARENT_MENU_ID); + } + return DEFAULT_PARENT_MENU_ID; + } + + /** + * 获取树编码 + * + * @param paramsObj 生成其他选项 + * @return 树编码 + */ + public static String getTreecode(JSONObject paramsObj) + { + if (paramsObj.containsKey(GenConstants.TREE_CODE)) + { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_CODE)); + } + return StringUtils.EMPTY; + } + + /** + * 获取树父编码 + * + * @param paramsObj 生成其他选项 + * @return 树父编码 + */ + public static String getTreeParentCode(JSONObject paramsObj) + { + if (paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) + { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_PARENT_CODE)); + } + return StringUtils.EMPTY; + } + + /** + * 获取树名称 + * + * @param paramsObj 生成其他选项 + * @return 树名称 + */ + public static String getTreeName(JSONObject paramsObj) + { + if (paramsObj.containsKey(GenConstants.TREE_NAME)) + { + return StringUtils.toCamelCase(paramsObj.getString(GenConstants.TREE_NAME)); + } + return StringUtils.EMPTY; + } + + /** + * 获取需要在哪一列上面显示展开按钮 + * + * @param genTable 业务表对象 + * @return 展开按钮列序号 + */ + public static int getExpandColumn(GenTable genTable) + { + String options = genTable.getOptions(); + JSONObject paramsObj = JSON.parseObject(options); + String treeName = paramsObj.getString(GenConstants.TREE_NAME); + int num = 0; + for (GenTableColumn column : genTable.getColumns()) + { + if (column.isList()) + { + num++; + String columnName = column.getColumnName(); + if (columnName.equals(treeName)) + { + break; + } + } + } + return num; + } +} diff --git a/ruoyi-generator/src/main/resources/generator.yml b/ruoyi-generator/src/main/resources/generator.yml new file mode 100644 index 0000000..24c12a3 --- /dev/null +++ b/ruoyi-generator/src/main/resources/generator.yml @@ -0,0 +1,12 @@ +# 代码生成 +gen: + # 作者 + author: rongxin + # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool + packageName: com.ruoyi.business + # 自动去除表前缀,默认是false + autoRemovePre: false + # 表前缀(生成类名不会包含表前缀,多个用逗号分隔) + tablePrefix: sys_ + # 是否允许生成文件覆盖到本地(自定义路径),默认不允许 + allowOverwrite: false diff --git a/ruoyi-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml b/ruoyi-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml new file mode 100644 index 0000000..52857e8 --- /dev/null +++ b/ruoyi-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select column_id, table_id, column_name, column_comment, column_type, java_type, java_field, is_pk, is_increment, is_required, is_insert, is_edit, is_list, is_query, query_type, html_type, dict_type, sort, create_by, create_time, update_by, update_time from gen_table_column + + + + + + + + insert into gen_table_column ( + table_id, + column_name, + column_comment, + column_type, + java_type, + java_field, + is_pk, + is_increment, + is_required, + is_insert, + is_edit, + is_list, + is_query, + query_type, + html_type, + dict_type, + sort, + create_by, + create_time + )values( + #{tableId}, + #{columnName}, + #{columnComment}, + #{columnType}, + #{javaType}, + #{javaField}, + #{isPk}, + #{isIncrement}, + #{isRequired}, + #{isInsert}, + #{isEdit}, + #{isList}, + #{isQuery}, + #{queryType}, + #{htmlType}, + #{dictType}, + #{sort}, + #{createBy}, + sysdate() + ) + + + + update gen_table_column + + column_comment = #{columnComment}, + java_type = #{javaType}, + java_field = #{javaField}, + is_insert = #{isInsert}, + is_edit = #{isEdit}, + is_list = #{isList}, + is_query = #{isQuery}, + is_required = #{isRequired}, + query_type = #{queryType}, + html_type = #{htmlType}, + dict_type = #{dictType}, + sort = #{sort}, + update_by = #{updateBy}, + update_time = sysdate() + + where column_id = #{columnId} + + + + delete from gen_table_column where table_id in + + #{tableId} + + + + + delete from gen_table_column where column_id in + + #{item.columnId} + + + + \ No newline at end of file diff --git a/ruoyi-generator/src/main/resources/mapper/generator/GenTableMapper.xml b/ruoyi-generator/src/main/resources/mapper/generator/GenTableMapper.xml new file mode 100644 index 0000000..d1110f7 --- /dev/null +++ b/ruoyi-generator/src/main/resources/mapper/generator/GenTableMapper.xml @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select table_id, table_name, table_comment, sub_table_name, sub_table_fk_name, class_name, tpl_category, tpl_web_type, package_name, module_name, business_name, function_name, function_author, gen_type, gen_path, options, create_by, create_time, update_by, update_time, remark from gen_table + + + + + + + + + + + + + + + + + + insert into gen_table ( + table_name, + table_comment, + class_name, + tpl_category, + tpl_web_type, + package_name, + module_name, + business_name, + function_name, + function_author, + gen_type, + gen_path, + remark, + create_by, + create_time + )values( + #{tableName}, + #{tableComment}, + #{className}, + #{tplCategory}, + #{tplWebType}, + #{packageName}, + #{moduleName}, + #{businessName}, + #{functionName}, + #{functionAuthor}, + #{genType}, + #{genPath}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + ${sql} + + + + update gen_table + + table_name = #{tableName}, + table_comment = #{tableComment}, + sub_table_name = #{subTableName}, + sub_table_fk_name = #{subTableFkName}, + class_name = #{className}, + function_author = #{functionAuthor}, + gen_type = #{genType}, + gen_path = #{genPath}, + tpl_category = #{tplCategory}, + tpl_web_type = #{tplWebType}, + package_name = #{packageName}, + module_name = #{moduleName}, + business_name = #{businessName}, + function_name = #{functionName}, + options = #{options}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where table_id = #{tableId} + + + + delete from gen_table where table_id in + + #{tableId} + + + + \ No newline at end of file diff --git a/ruoyi-generator/src/main/resources/vm/java/controller.java.vm b/ruoyi-generator/src/main/resources/vm/java/controller.java.vm new file mode 100644 index 0000000..a891de6 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/java/controller.java.vm @@ -0,0 +1,197 @@ +package ${packageName}.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.enums.BusinessType; +import ${packageName}.domain.${ClassName}; +import ${packageName}.service.I${ClassName}Service; +import com.ruoyi.common.utils.poi.ExcelUtil; +#if($table.crud || $table.sub) +import com.ruoyi.common.core.page.TableDataInfo; +#elseif($table.tree) +#end + +/** + * ${functionName}Controller + * + * @author ${author} + * @date ${datetime} + */ +@RestController +@RequestMapping("/${moduleName}/${businessName}") +public class ${ClassName}Controller extends BaseController +{ + @Autowired + private I${ClassName}Service ${className}Service; + + /** + * 查询${functionName}列表 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:list')") + @GetMapping("/list") +#if($table.crud || $table.sub) + public TableDataInfo list(${ClassName} ${className}) + { + startPage(); + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + return getDataTable(list); + } +#elseif($table.tree) + public AjaxResult list(${ClassName} ${className}) + { + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + return success(list); + } +#end + + + /** + * 导出${functionName}列表 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:export')") + @Log(title = "${functionName}", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, ${ClassName} ${className}) + { + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + ExcelUtil<${ClassName}> util = new ExcelUtil<${ClassName}>(${ClassName}.class); + util.exportExcel(response, list, "${functionName}数据"); + } + + /** + * ${functionName}导入模板 + */ + @RequiresPermissions("${permissionPrefix}:view") + @GetMapping("/importTemplate") + @ResponseBody + public AjaxResult importTemplate() { + ExcelUtil<${ClassName}> util = new ExcelUtil<${ClassName}>(${ClassName}.class); + return util.importTemplateExcel("${functionName}数据"); + } + + /** + * ${functionName}导入 + */ + @Log(title = "${functionName}", businessType = BusinessType.IMPORT) + @RequiresPermissions("${permissionPrefix}:import") + @PostMapping("/importData") + @ResponseBody + public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception { + ExcelUtil<${ClassName}> util = new ExcelUtil<${ClassName}>(${ClassName}.class); + List<${ClassName}> list = util.importExcel(file.getInputStream(), 0); + String message = ${className}Service.import${ClassName}(list, updateSupport, getSysUser()); + return AjaxResult.success(message); + } + + /** + * 获取${functionName}详细信息 + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:query')") + @GetMapping(value = "/{${pkColumn.javaField}}") + public AjaxResult getInfo(@PathVariable("${pkColumn.javaField}") ${pkColumn.javaType} ${pkColumn.javaField}) + { + return success(${className}Service.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField})); + } + + /** + * 新增${functionName} + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:add')") + @Log(title = "${functionName}", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody ${ClassName} ${className}) + { + return toAjax(${className}Service.insert${ClassName}(${className})); + } + + /** + * 修改${functionName} + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:edit')") + @Log(title = "${functionName}", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody ${ClassName} ${className}) + { + return toAjax(${className}Service.update${ClassName}(${className})); + } + + /** + * 删除${functionName} + */ + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:remove')") + @Log(title = "${functionName}", businessType = BusinessType.DELETE) + @DeleteMapping("/{${pkColumn.javaField}s}") + public AjaxResult remove(@PathVariable ${pkColumn.javaType}[] ${pkColumn.javaField}s) + { + return toAjax(${className}Service.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s)); + } + + /** + * 打印${functionName} + */ + @RequiresPermissions("${permissionPrefix}:print") + @Log(title = "${functionName}", businessType = BusinessType.PRINT) + @GetMapping("/print") + public void printPdf(${ClassName} ${className}, HttpServletResponse response) throws Exception{ + ${className}.setBookId(getSysUser().getLoginBookid()); + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + TranslateUtils.translateList(list, false); + + PdfProperty pdf = new PdfProperty(); + pdf.setTitle("${functionName}"); + //pdf.setRowHeight(20f); + + FinanceBook book = financeBookService.selectFinanceBookById(getSysUser().getLoginBookid()); + ShoulderItem shoulder = new ShoulderItem(); + shoulder.setLeftItem("编制单位:"); + shoulder.setCenterItem("日期:" ); + shoulder.setRightItem("单位:"); + pdf.setShoulder(shoulder); + + float[] columnWidth = new float[]{20, 40, 10, 10, 10, 10}; + pdf.setColumnWidth(columnWidth); + String[] header = new String[]{"A列", "B列", "C列", "D列", "E列", "F列"}; + pdf.setHeader(header); + int[] aligns = new int[]{0, 0, 1, 1, 2, 2}; + pdf.setAligns(aligns); + + List contentList = Lists.newArrayList(); + list.forEach(a ->{ + String[] str = new String[6]; + str[0] = a.getBookName(); + str[1] = a.getOrgCodeCertNum(); + str[2] = a.getDeptName(); + str[3] = a.getAccountant(); + str[4] = a.getAccountantPhone(); + str[5] = a.getCurrentDay(); + contentList.add(str); + }); + pdf.setContentList(contentList); + + PageSet ps = new PageSet(); + ps.setPaperWidth(595.0f); + ps.setPaperHeight(842.0f); + ps.setPrintDirection("1"); + ps.setTableTotalWidth(520); + ps.setMarginLeft(50); + ps.setMarginRight(50); + ps.setMarginTop(50); + ps.setMarginBottom(50); + pdf.setPageSet(ps); + + PdfUtils.initPdf(response, pdf); + } +} diff --git a/ruoyi-generator/src/main/resources/vm/java/domain.java.vm b/ruoyi-generator/src/main/resources/vm/java/domain.java.vm new file mode 100644 index 0000000..b236ef0 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/java/domain.java.vm @@ -0,0 +1,105 @@ +package ${packageName}.domain; + +#foreach ($import in $importList) +import ${import}; +#end +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +#if($table.crud || $table.sub) +import com.ruoyi.common.core.domain.BaseEntity; +#elseif($table.tree) +import com.ruoyi.common.core.domain.TreeEntity; +#end + +/** + * ${functionName}对象 ${tableName} + * + * @author ${author} + * @date ${datetime} + */ +#if($table.crud || $table.sub) +#set($Entity="BaseEntity") +#elseif($table.tree) +#set($Entity="TreeEntity") +#end +public class ${ClassName} extends ${Entity} +{ + private static final long serialVersionUID = 1L; + +#foreach ($column in $columns) +#if(!$table.isSuperColumn($column.javaField)) + /** $column.columnComment */ +#if($column.list) +#set($parentheseIndex=$column.columnComment.indexOf("(")) +#if($parentheseIndex != -1) +#set($comment=$column.columnComment.substring(0, $parentheseIndex)) +#else +#set($comment=$column.columnComment) +#end +#if($parentheseIndex != -1) + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") +#elseif($column.javaType == 'Date') + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd") +#else + @Excel(name = "${comment}") +#end +#end + private $column.javaType $column.javaField; + +#end +#end +#if($table.sub) + /** $table.subTable.functionName信息 */ + private List<${subClassName}> ${subclassName}List; + +#end +#foreach ($column in $columns) +#if(!$table.isSuperColumn($column.javaField)) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + public void set${AttrName}($column.javaType $column.javaField) + { + this.$column.javaField = $column.javaField; + } + + public $column.javaType get${AttrName}() + { + return $column.javaField; + } + +#end +#end +#if($table.sub) + public List<${subClassName}> get${subClassName}List() + { + return ${subclassName}List; + } + + public void set${subClassName}List(List<${subClassName}> ${subclassName}List) + { + this.${subclassName}List = ${subclassName}List; + } + +#end + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) +#foreach ($column in $columns) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + .append("${column.javaField}", get${AttrName}()) +#end +#if($table.sub) + .append("${subclassName}List", get${subClassName}List()) +#end + .toString(); + } +} diff --git a/ruoyi-generator/src/main/resources/vm/java/mapper.java.vm b/ruoyi-generator/src/main/resources/vm/java/mapper.java.vm new file mode 100644 index 0000000..c576602 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/java/mapper.java.vm @@ -0,0 +1,107 @@ +package ${packageName}.mapper; + +import java.util.List; +import ${packageName}.domain.${ClassName}; +#if($table.sub) +import ${packageName}.domain.${subClassName}; +#end + +/** + * ${functionName}Mapper接口 + * + * @author ${author} + * @date ${datetime} + */ +public interface ${ClassName}Mapper +{ + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName}集合 + */ + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}); + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int insert${ClassName}(${ClassName} ${className}); + + /** + * 批量新增${functionName} + * + * @param list ${functionName} + * @return 结果 + */ + public int insert${ClassName}Batch(List<${ClassName}> list); + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int update${ClassName}(${ClassName} ${className}); + + /** + * 批量修改 ${functionName} + * + * @param list ${functionName} + * @return 结果 + */ + public int update${ClassName}Batch(List<${ClassName}> list); + + /** + * 删除${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的数据主键集合 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); +#if($table.sub) + + /** + * 批量删除${subTable.functionName} + * + * @param ${pkColumn.javaField}s 需要删除的数据主键集合 + * @return 结果 + */ + public int delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); + + /** + * 批量新增${subTable.functionName} + * + * @param ${subclassName}List ${subTable.functionName}列表 + * @return 结果 + */ + public int batch${subClassName}(List<${subClassName}> ${subclassName}List); + + + /** + * 通过${functionName}主键删除${subTable.functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}ID + * @return 结果 + */ + public int delete${subClassName}By${subTableFkClassName}(${pkColumn.javaType} ${pkColumn.javaField}); +#end +} diff --git a/ruoyi-generator/src/main/resources/vm/java/service.java.vm b/ruoyi-generator/src/main/resources/vm/java/service.java.vm new file mode 100644 index 0000000..daca176 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/java/service.java.vm @@ -0,0 +1,87 @@ +package ${packageName}.service; + +import java.util.List; +import ${packageName}.domain.${ClassName}; + +/** + * ${functionName}Service接口 + * + * @author ${author} + * @date ${datetime} + */ +public interface I${ClassName}Service +{ + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName}集合 + */ + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}); + + /** + * 导入${functionName}数据 + * + * @param list ${functionName}数据列表 + * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据 + * @param user 操作用户 + * @return 结果 + */ + public String import${ClassName}(List<${ClassName}> list, Boolean isUpdateSupport, SysUser user); + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int insert${ClassName}(${ClassName} ${className}); + + /** + * 批量新增${functionName} + * + * @param list ${functionName} + * @return 结果 + */ + public int insert${ClassName}Batch(List<${ClassName}> list); + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ + public int update${ClassName}(${ClassName} ${className}); + + /** + * 批量修改 ${functionName} + * + * @param list ${functionName} + * @return 结果 + */ + public int update${ClassName}Batch(List<${ClassName}> list); + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的${functionName}主键集合 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); + + /** + * 删除${functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); +} diff --git a/ruoyi-generator/src/main/resources/vm/java/serviceImpl.java.vm b/ruoyi-generator/src/main/resources/vm/java/serviceImpl.java.vm new file mode 100644 index 0000000..7d6054f --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/java/serviceImpl.java.vm @@ -0,0 +1,280 @@ +package ${packageName}.service.impl; + +import java.util.List; +#foreach ($column in $columns) +#if($column.javaField == 'createTime' || $column.javaField == 'updateTime') +import com.ruoyi.common.utils.DateUtils; +#break +#end +#end +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +#if($table.sub) +import java.util.ArrayList; +import com.ruoyi.common.utils.StringUtils; +import org.springframework.transaction.annotation.Transactional; +import ${packageName}.domain.${subClassName}; +#end +import ${packageName}.mapper.${ClassName}Mapper; +import ${packageName}.domain.${ClassName}; +import ${packageName}.service.I${ClassName}Service; + +/** + * ${functionName}Service业务层处理 + * + * @author ${author} + * @date ${datetime} + */ +@Service +public class ${ClassName}ServiceImpl implements I${ClassName}Service +{ + @Autowired + private ${ClassName}Mapper ${className}Mapper; + + /** + * 查询${functionName} + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return ${functionName} + */ + @Override + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}) + { + return ${className}Mapper.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}); + } + + /** + * 查询${functionName}列表 + * + * @param ${className} ${functionName} + * @return ${functionName} + */ + @Override + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}) + { + return ${className}Mapper.select${ClassName}List(${className}); + } + + /** + * 导入${functionName}数据 + * + * @param list ${functionName}数据列表 + * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据 + * @param user 操作用户 + * @return 结果 + */ + @Override + @Transactional + public String import${ClassName}(List<${ClassName}> list, Boolean isUpdateSupport, SysUser user) { + if (StringUtils.isEmpty(list)) { + throw new ServiceException("导入${functionName}数据不能为空!"); + } + String operName = user.getLoginName(); + + int successNum = 0; + int failureNum = 0; + StringBuilder successMsg = new StringBuilder(); + StringBuilder failureMsg = new StringBuilder(); + List<${ClassName}> insertList = Lists.newArrayList(); + List<${ClassName}> updateList = Lists.newArrayList(); + + // 检验excel表中 【xxx】 是否存在重复,有重复的终止导入 + List repeatList = list.stream().collect(Collectors.groupingBy(${ClassName}::getCode, Collectors.counting())) + .entrySet().stream().filter(e -> e.getValue() > 1).map(Map.Entry::getKey).collect(Collectors.toList()); + if (StringUtils.isNotEmpty(repeatList)) { + failureMsg.insert(0, "很抱歉,存在重复项,导入失败!共 " + repeatList.size() + " 个重复错错误如下:"); + for (int i = 0; i < repeatList.size(); i++) { + String item = repeatList.get(i); + failureNum++; + failureMsg.append("
" + failureNum + "、项目名 " + item + " 重复"); + } + return failureMsg.toString(); + } + + // 缓存已存在的数据 + ${ClassName} ${className} = new ${ClassName}(); + List<${ClassName}> ${className}List = ${className}Mapper.select${ClassName}List(${className}); + + for (${ClassName} item : list) { + // 一定层次上校验并跳过空白行,最好是不空的唯一字段 + if(StringUtils.isEmpty(item.getName())){ + continue; + } + // 验证是否存在这个${functionName}信息 + Long id = item.getId(); + List<${ClassName}> filters = ${className}List.stream().filter(a -> a.getId().equals(id)).collect(Collectors.toList()); + if (StringUtils.isEmpty(filters)) { //不存在时,直接插入 + item.setCreateBy(operName); + item.setCreateTime(DateUtils.getNowDate()); + insertList.add(item); + successNum++; + } else { //存在时 + if(isUpdateSupport){ //勾选则更新 + item.setUpdateBy(operName); + item.setUpdateTime(DateUtils.getNowDate()); + item.setId(filters.get(0).getId()); + updateList.add(item); + successNum++; + } + } + } + + // 执行更新或者保存 + List> insertSplists = ListUtils.partition(insertList, 50); + insertSplists.forEach(insertSplist ->{ + ${className}Mapper.insert${ClassName}Batch(insertSplist); + }); + List> updateSplists = ListUtils.partition(updateList, 30); + updateSplists.forEach(updateSplist ->{ + ${className}Mapper.update${ClassName}Batch(updateSplist); + }); + + successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条。"); + return successMsg.toString(); + } + + + /** + * 新增${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int insert${ClassName}(${ClassName} ${className}) + { +#foreach ($column in $columns) +#if($column.javaField == 'createTime') + ${className}.setCreateTime(DateUtils.getNowDate()); +#end +#end +#if($table.sub) + int rows = ${className}Mapper.insert${ClassName}(${className}); + insert${subClassName}(${className}); + return rows; +#else + return ${className}Mapper.insert${ClassName}(${className}); +#end + } + + /** + * 批量新增${functionName} + * + * @param list ${functionName} + * @return 结果 + */ + @Override + @Transactional + public int insert${ClassName}Batch(List<${ClassName}> list){ + List> splists = ListUtils.partition(list, 50); + splists.forEach(splist->{ + ${className}Mapper.insert${ClassName}Batch(splist); + }); + return 1; + } + + /** + * 修改${functionName} + * + * @param ${className} ${functionName} + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int update${ClassName}(${ClassName} ${className}) + { +#foreach ($column in $columns) +#if($column.javaField == 'updateTime') + ${className}.setUpdateTime(DateUtils.getNowDate()); +#end +#end +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}(${className}.get${pkColumn.capJavaField}()); + insert${subClassName}(${className}); +#end + return ${className}Mapper.update${ClassName}(${className}); + } + + /** + * 批量修改 ${functionName} + * + * @param list ${functionName} + * @return 结果 + */ + @Override + @Transactional + public int update${ClassName}Batch(List<${ClassName}> list) { + List> splists = ListUtils.partition(list, 30); + splists.forEach(splist->{ + ${className}Mapper.update${ClassName}Batch(splist); + }); + return 1; + } + + /** + * 批量删除${functionName} + * + * @param ${pkColumn.javaField}s 需要删除的${functionName}主键 + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s) + { +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaField}s); +#end + return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s); + } + + /** + * 删除${functionName}信息 + * + * @param ${pkColumn.javaField} ${functionName}主键 + * @return 结果 + */ +#if($table.sub) + @Transactional +#end + @Override + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}) + { +#if($table.sub) + ${className}Mapper.delete${subClassName}By${subTableFkClassName}(${pkColumn.javaField}); +#end + return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}); + } +#if($table.sub) + + /** + * 新增${subTable.functionName}信息 + * + * @param ${className} ${functionName}对象 + */ + public void insert${subClassName}(${ClassName} ${className}) + { + List<${subClassName}> ${subclassName}List = ${className}.get${subClassName}List(); + ${pkColumn.javaType} ${pkColumn.javaField} = ${className}.get${pkColumn.capJavaField}(); + if (StringUtils.isNotNull(${subclassName}List)) + { + List<${subClassName}> list = new ArrayList<${subClassName}>(); + for (${subClassName} ${subclassName} : ${subclassName}List) + { + ${subclassName}.set${subTableFkClassName}(${pkColumn.javaField}); + list.add(${subclassName}); + } + if (list.size() > 0) + { + ${className}Mapper.batch${subClassName}(list); + } + } + } +#end +} diff --git a/ruoyi-generator/src/main/resources/vm/java/sub-domain.java.vm b/ruoyi-generator/src/main/resources/vm/java/sub-domain.java.vm new file mode 100644 index 0000000..a3f53eb --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/java/sub-domain.java.vm @@ -0,0 +1,76 @@ +package ${packageName}.domain; + +#foreach ($import in $subImportList) +import ${import}; +#end +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * ${subTable.functionName}对象 ${subTableName} + * + * @author ${author} + * @date ${datetime} + */ +public class ${subClassName} extends BaseEntity +{ + private static final long serialVersionUID = 1L; + +#foreach ($column in $subTable.columns) +#if(!$table.isSuperColumn($column.javaField)) + /** $column.columnComment */ +#if($column.list) +#set($parentheseIndex=$column.columnComment.indexOf("(")) +#if($parentheseIndex != -1) +#set($comment=$column.columnComment.substring(0, $parentheseIndex)) +#else +#set($comment=$column.columnComment) +#end +#if($parentheseIndex != -1) + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") +#elseif($column.javaType == 'Date') + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd") +#else + @Excel(name = "${comment}") +#end +#end + private $column.javaType $column.javaField; + +#end +#end +#foreach ($column in $subTable.columns) +#if(!$table.isSuperColumn($column.javaField)) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + public void set${AttrName}($column.javaType $column.javaField) + { + this.$column.javaField = $column.javaField; + } + + public $column.javaType get${AttrName}() + { + return $column.javaField; + } +#end +#end + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) +#foreach ($column in $subTable.columns) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + .append("${column.javaField}", get${AttrName}()) +#end + .toString(); + } +} diff --git a/ruoyi-generator/src/main/resources/vm/js/api.js.vm b/ruoyi-generator/src/main/resources/vm/js/api.js.vm new file mode 100644 index 0000000..1a87718 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/js/api.js.vm @@ -0,0 +1,54 @@ +import request from '@/utils/request' + +// 查询${functionName}列表 +export function list${BusinessName}(query) { + return request({ + url: '/${moduleName}/${businessName}/list', + method: 'get', + params: query + }) +} + +// 查询${functionName}详细 +export function get${BusinessName}(${pkColumn.javaField}) { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'get' + }) +} + + +// 新增${functionName} +export function add${BusinessName}(data) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'post', + data: data + }) +} + +// 修改${functionName} +export function update${BusinessName}(data) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'put', + data: data + }) +} + +// 删除${functionName} +export function del${BusinessName}(${pkColumn.javaField}) { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'delete' + }) +} + +// 打印${functionName} +export function print${BusinessName}(query) { + return request({ + url: '/${moduleName}/${businessName}/print', + method: 'get', + params: query + }) +} diff --git a/ruoyi-generator/src/main/resources/vm/sql/sql.vm b/ruoyi-generator/src/main/resources/vm/sql/sql.vm new file mode 100644 index 0000000..425278f --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/sql/sql.vm @@ -0,0 +1,32 @@ +-- 菜单 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}', '${parentMenuId}', '1', '${businessName}', '${moduleName}/${businessName}/index', 1, 0, 'C', '0', '0', '${permissionPrefix}:list', '#', 'admin', sysdate(), '', null, '${functionName}菜单'); + +-- 按钮父菜单ID +SELECT @parentId := LAST_INSERT_ID(); + +-- 按钮 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}查询', @parentId, '1', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:query', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}新增', @parentId, '2', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:add', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}修改', @parentId, '3', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:edit', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}删除', @parentId, '4', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:remove', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}导出', @parentId, '5', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:export', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}导入', @parentId, '5', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:export', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}打印', @parentId, '5', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:import', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}附件', @parentId, '5', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:attach', '#', 'admin', sysdate(), '', null, ''); + diff --git a/ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm b/ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm new file mode 100644 index 0000000..4e35fc9 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm @@ -0,0 +1,505 @@ + + + diff --git a/ruoyi-generator/src/main/resources/vm/vue/index.vue.vm b/ruoyi-generator/src/main/resources/vm/vue/index.vue.vm new file mode 100644 index 0000000..6c409a2 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/vue/index.vue.vm @@ -0,0 +1,638 @@ + + + diff --git a/ruoyi-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm b/ruoyi-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm new file mode 100644 index 0000000..765a5e3 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm @@ -0,0 +1,474 @@ + + + diff --git a/ruoyi-generator/src/main/resources/vm/vue/v3/index.vue.vm b/ruoyi-generator/src/main/resources/vm/vue/v3/index.vue.vm new file mode 100644 index 0000000..936b465 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/vue/v3/index.vue.vm @@ -0,0 +1,590 @@ + + + diff --git a/ruoyi-generator/src/main/resources/vm/xml/mapper.xml.vm b/ruoyi-generator/src/main/resources/vm/xml/mapper.xml.vm new file mode 100644 index 0000000..6346f66 --- /dev/null +++ b/ruoyi-generator/src/main/resources/vm/xml/mapper.xml.vm @@ -0,0 +1,176 @@ + + + + + +#foreach ($column in $columns) + +#end + +#if($table.sub) + + + + + + +#foreach ($column in $subTable.columns) + +#end + +#end + + + select#foreach($column in $columns) $column.columnName#if($foreach.count != $columns.size()),#end#end from ${tableName} + + + + + +#if($table.sub) + + +#end + + + insert into ${tableName} + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + $column.columnName, +#end +#end + + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + #{$column.javaField}, +#end +#end + + + + + insert into ${tableName} + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + $column.columnName, +#end +#end + + values + + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + #{item.$column.javaField}, +#end +#end + + + + + + update ${tableName} + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName) + $column.columnName = #{$column.javaField}, +#end +#end + + where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + + + update ${tableName} + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName) + $column.columnName = #{item.$column.javaField}, +#end +#end + + where ${pkColumn.columnName} = #{item.${pkColumn.javaField}} + + + + + delete from ${tableName} where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + delete from ${tableName} where ${pkColumn.columnName} in + + #{${pkColumn.javaField}} + + +#if($table.sub) + + + delete from ${subTableName} where ${subTableFkName} in + + #{${subTableFkclassName}} + + + + + delete from ${subTableName} where ${subTableFkName} = #{${subTableFkclassName}} + + + + insert into ${subTableName}(#foreach($column in $subTable.columns) $column.columnName#if($foreach.count != $subTable.columns.size()),#end#end) values + + (#foreach($column in $subTable.columns) #{item.$column.javaField}#if($foreach.count != $subTable.columns.size()),#end#end) + + +#end + diff --git a/ruoyi-quartz/pom.xml b/ruoyi-quartz/pom.xml new file mode 100644 index 0000000..68e3e1d --- /dev/null +++ b/ruoyi-quartz/pom.xml @@ -0,0 +1,40 @@ + + + + ruoyi + com.ruoyi + 3.9.0 + + 4.0.0 + + ruoyi-quartz + + + quartz定时任务 + + + + + + + org.quartz-scheduler + quartz + + + com.mchange + c3p0 + + + + + + + com.ruoyi + ruoyi-common + + + + + \ No newline at end of file diff --git a/ruoyi-quartz/ruoyi-quartz.iml b/ruoyi-quartz/ruoyi-quartz.iml new file mode 100644 index 0000000..e12e9e6 --- /dev/null +++ b/ruoyi-quartz/ruoyi-quartz.iml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/config/ScheduleConfig.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/config/ScheduleConfig.java new file mode 100644 index 0000000..d4e065a --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/config/ScheduleConfig.java @@ -0,0 +1,57 @@ +//package com.ruoyi.quartz.config; +// +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.scheduling.quartz.SchedulerFactoryBean; +//import javax.sql.DataSource; +//import java.util.Properties; +// +///** +// * 定时任务配置(单机部署建议删除此类和qrtz数据库表,默认走内存会最高效) +// * +// * @author ruoyi +// */ +//@Configuration +//public class ScheduleConfig +//{ +// @Bean +// public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) +// { +// SchedulerFactoryBean factory = new SchedulerFactoryBean(); +// factory.setDataSource(dataSource); +// +// // quartz参数 +// Properties prop = new Properties(); +// prop.put("org.quartz.scheduler.instanceName", "RuoyiScheduler"); +// prop.put("org.quartz.scheduler.instanceId", "AUTO"); +// // 线程池配置 +// prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool"); +// prop.put("org.quartz.threadPool.threadCount", "20"); +// prop.put("org.quartz.threadPool.threadPriority", "5"); +// // JobStore配置 +// prop.put("org.quartz.jobStore.class", "org.springframework.scheduling.quartz.LocalDataSourceJobStore"); +// // 集群配置 +// prop.put("org.quartz.jobStore.isClustered", "true"); +// prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000"); +// prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "10"); +// prop.put("org.quartz.jobStore.txIsolationLevelSerializable", "true"); +// +// // sqlserver 启用 +// // prop.put("org.quartz.jobStore.selectWithLockSQL", "SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?"); +// prop.put("org.quartz.jobStore.misfireThreshold", "12000"); +// prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_"); +// factory.setQuartzProperties(prop); +// +// factory.setSchedulerName("RuoyiScheduler"); +// // 延时启动 +// factory.setStartupDelay(1); +// factory.setApplicationContextSchedulerContextKey("applicationContextKey"); +// // 可选,QuartzScheduler +// // 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了 +// factory.setOverwriteExistingJobs(true); +// // 设置自动启动,默认为true +// factory.setAutoStartup(true); +// +// return factory; +// } +//} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobController.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobController.java new file mode 100644 index 0000000..f11aebf --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobController.java @@ -0,0 +1,185 @@ +package com.ruoyi.quartz.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.quartz.SchedulerException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.exception.job.TaskException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.quartz.domain.SysJob; +import com.ruoyi.quartz.service.ISysJobService; +import com.ruoyi.quartz.util.CronUtils; +import com.ruoyi.quartz.util.ScheduleUtils; + +/** + * 调度任务信息操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/job") +public class SysJobController extends BaseController +{ + @Autowired + private ISysJobService jobService; + + /** + * 查询定时任务列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:list')") + @GetMapping("/list") + public TableDataInfo list(SysJob sysJob) + { + startPage(); + List list = jobService.selectJobList(sysJob); + return getDataTable(list); + } + + /** + * 导出定时任务列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:export')") + @Log(title = "定时任务", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, SysJob sysJob) + { + List list = jobService.selectJobList(sysJob); + ExcelUtil util = new ExcelUtil(SysJob.class); + util.exportExcel(response, list, "定时任务"); + } + + /** + * 获取定时任务详细信息 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:query')") + @GetMapping(value = "/{jobId}") + public AjaxResult getInfo(@PathVariable("jobId") Long jobId) + { + return success(jobService.selectJobById(jobId)); + } + + /** + * 新增定时任务 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:add')") + @Log(title = "定时任务", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody SysJob job) throws SchedulerException, TaskException + { + if (!CronUtils.isValid(job.getCronExpression())) + { + return error("新增任务'" + job.getJobName() + "'失败,Cron表达式不正确"); + } + else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS })) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS })) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串存在违规"); + } + else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) + { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不在白名单内"); + } + job.setCreateBy(getUsername()); + return toAjax(jobService.insertJob(job)); + } + + /** + * 修改定时任务 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:edit')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody SysJob job) throws SchedulerException, TaskException + { + if (!CronUtils.isValid(job.getCronExpression())) + { + return error("修改任务'" + job.getJobName() + "'失败,Cron表达式不正确"); + } + else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS })) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS })) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用"); + } + else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串存在违规"); + } + else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) + { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不在白名单内"); + } + job.setUpdateBy(getUsername()); + return toAjax(jobService.updateJob(job)); + } + + /** + * 定时任务状态修改 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysJob job) throws SchedulerException + { + SysJob newJob = jobService.selectJobById(job.getJobId()); + newJob.setStatus(job.getStatus()); + return toAjax(jobService.changeStatus(newJob)); + } + + /** + * 定时任务立即执行一次 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping("/run") + public AjaxResult run(@RequestBody SysJob job) throws SchedulerException + { + boolean result = jobService.run(job); + return result ? success() : error("任务不存在或已过期!"); + } + + /** + * 删除定时任务 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:remove')") + @Log(title = "定时任务", businessType = BusinessType.DELETE) + @DeleteMapping("/{jobIds}") + public AjaxResult remove(@PathVariable Long[] jobIds) throws SchedulerException + { + jobService.deleteJobByIds(jobIds); + return success(); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobLogController.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobLogController.java new file mode 100644 index 0000000..62ecbab --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/controller/SysJobLogController.java @@ -0,0 +1,92 @@ +package com.ruoyi.quartz.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.quartz.domain.SysJobLog; +import com.ruoyi.quartz.service.ISysJobLogService; + +/** + * 调度日志操作处理 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/monitor/jobLog") +public class SysJobLogController extends BaseController +{ + @Autowired + private ISysJobLogService jobLogService; + + /** + * 查询定时任务调度日志列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:list')") + @GetMapping("/list") + public TableDataInfo list(SysJobLog sysJobLog) + { + startPage(); + List list = jobLogService.selectJobLogList(sysJobLog); + return getDataTable(list); + } + + /** + * 导出定时任务调度日志列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:export')") + @Log(title = "任务调度日志", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, SysJobLog sysJobLog) + { + List list = jobLogService.selectJobLogList(sysJobLog); + ExcelUtil util = new ExcelUtil(SysJobLog.class); + util.exportExcel(response, list, "调度日志"); + } + + /** + * 根据调度编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:query')") + @GetMapping(value = "/{jobLogId}") + public AjaxResult getInfo(@PathVariable Long jobLogId) + { + return success(jobLogService.selectJobLogById(jobLogId)); + } + + + /** + * 删除定时任务调度日志 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:remove')") + @Log(title = "定时任务调度日志", businessType = BusinessType.DELETE) + @DeleteMapping("/{jobLogIds}") + public AjaxResult remove(@PathVariable Long[] jobLogIds) + { + return toAjax(jobLogService.deleteJobLogByIds(jobLogIds)); + } + + /** + * 清空定时任务调度日志 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:remove')") + @Log(title = "调度日志", businessType = BusinessType.CLEAN) + @DeleteMapping("/clean") + public AjaxResult clean() + { + jobLogService.cleanJobLog(); + return success(); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJob.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJob.java new file mode 100644 index 0000000..1f49695 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJob.java @@ -0,0 +1,171 @@ +package com.ruoyi.quartz.domain; + +import java.util.Date; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.constant.ScheduleConstants; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.quartz.util.CronUtils; + +/** + * 定时任务调度表 sys_job + * + * @author ruoyi + */ +public class SysJob extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 任务ID */ + @Excel(name = "任务序号", cellType = ColumnType.NUMERIC) + private Long jobId; + + /** 任务名称 */ + @Excel(name = "任务名称") + private String jobName; + + /** 任务组名 */ + @Excel(name = "任务组名") + private String jobGroup; + + /** 调用目标字符串 */ + @Excel(name = "调用目标字符串") + private String invokeTarget; + + /** cron执行表达式 */ + @Excel(name = "执行表达式 ") + private String cronExpression; + + /** cron计划策略 */ + @Excel(name = "计划策略 ", readConverterExp = "0=默认,1=立即触发执行,2=触发一次执行,3=不触发立即执行") + private String misfirePolicy = ScheduleConstants.MISFIRE_DEFAULT; + + /** 是否并发执行(0允许 1禁止) */ + @Excel(name = "并发执行", readConverterExp = "0=允许,1=禁止") + private String concurrent; + + /** 任务状态(0正常 1暂停) */ + @Excel(name = "任务状态", readConverterExp = "0=正常,1=暂停") + private String status; + + public Long getJobId() + { + return jobId; + } + + public void setJobId(Long jobId) + { + this.jobId = jobId; + } + + @NotBlank(message = "任务名称不能为空") + @Size(min = 0, max = 64, message = "任务名称不能超过64个字符") + public String getJobName() + { + return jobName; + } + + public void setJobName(String jobName) + { + this.jobName = jobName; + } + + public String getJobGroup() + { + return jobGroup; + } + + public void setJobGroup(String jobGroup) + { + this.jobGroup = jobGroup; + } + + @NotBlank(message = "调用目标字符串不能为空") + @Size(min = 0, max = 500, message = "调用目标字符串长度不能超过500个字符") + public String getInvokeTarget() + { + return invokeTarget; + } + + public void setInvokeTarget(String invokeTarget) + { + this.invokeTarget = invokeTarget; + } + + @NotBlank(message = "Cron执行表达式不能为空") + @Size(min = 0, max = 255, message = "Cron执行表达式不能超过255个字符") + public String getCronExpression() + { + return cronExpression; + } + + public void setCronExpression(String cronExpression) + { + this.cronExpression = cronExpression; + } + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + public Date getNextValidTime() + { + if (StringUtils.isNotEmpty(cronExpression)) + { + return CronUtils.getNextExecution(cronExpression); + } + return null; + } + + public String getMisfirePolicy() + { + return misfirePolicy; + } + + public void setMisfirePolicy(String misfirePolicy) + { + this.misfirePolicy = misfirePolicy; + } + + public String getConcurrent() + { + return concurrent; + } + + public void setConcurrent(String concurrent) + { + this.concurrent = concurrent; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("jobId", getJobId()) + .append("jobName", getJobName()) + .append("jobGroup", getJobGroup()) + .append("cronExpression", getCronExpression()) + .append("nextValidTime", getNextValidTime()) + .append("misfirePolicy", getMisfirePolicy()) + .append("concurrent", getConcurrent()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJobLog.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJobLog.java new file mode 100644 index 0000000..121c035 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/domain/SysJobLog.java @@ -0,0 +1,155 @@ +package com.ruoyi.quartz.domain; + +import java.util.Date; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 定时任务调度日志表 sys_job_log + * + * @author ruoyi + */ +public class SysJobLog extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** ID */ + @Excel(name = "日志序号") + private Long jobLogId; + + /** 任务名称 */ + @Excel(name = "任务名称") + private String jobName; + + /** 任务组名 */ + @Excel(name = "任务组名") + private String jobGroup; + + /** 调用目标字符串 */ + @Excel(name = "调用目标字符串") + private String invokeTarget; + + /** 日志信息 */ + @Excel(name = "日志信息") + private String jobMessage; + + /** 执行状态(0正常 1失败) */ + @Excel(name = "执行状态", readConverterExp = "0=正常,1=失败") + private String status; + + /** 异常信息 */ + @Excel(name = "异常信息") + private String exceptionInfo; + + /** 开始时间 */ + private Date startTime; + + /** 停止时间 */ + private Date stopTime; + + public Long getJobLogId() + { + return jobLogId; + } + + public void setJobLogId(Long jobLogId) + { + this.jobLogId = jobLogId; + } + + public String getJobName() + { + return jobName; + } + + public void setJobName(String jobName) + { + this.jobName = jobName; + } + + public String getJobGroup() + { + return jobGroup; + } + + public void setJobGroup(String jobGroup) + { + this.jobGroup = jobGroup; + } + + public String getInvokeTarget() + { + return invokeTarget; + } + + public void setInvokeTarget(String invokeTarget) + { + this.invokeTarget = invokeTarget; + } + + public String getJobMessage() + { + return jobMessage; + } + + public void setJobMessage(String jobMessage) + { + this.jobMessage = jobMessage; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getExceptionInfo() + { + return exceptionInfo; + } + + public void setExceptionInfo(String exceptionInfo) + { + this.exceptionInfo = exceptionInfo; + } + + public Date getStartTime() + { + return startTime; + } + + public void setStartTime(Date startTime) + { + this.startTime = startTime; + } + + public Date getStopTime() + { + return stopTime; + } + + public void setStopTime(Date stopTime) + { + this.stopTime = stopTime; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("jobLogId", getJobLogId()) + .append("jobName", getJobName()) + .append("jobGroup", getJobGroup()) + .append("jobMessage", getJobMessage()) + .append("status", getStatus()) + .append("exceptionInfo", getExceptionInfo()) + .append("startTime", getStartTime()) + .append("stopTime", getStopTime()) + .toString(); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobLogMapper.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobLogMapper.java new file mode 100644 index 0000000..727d916 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobLogMapper.java @@ -0,0 +1,64 @@ +package com.ruoyi.quartz.mapper; + +import java.util.List; +import com.ruoyi.quartz.domain.SysJobLog; + +/** + * 调度任务日志信息 数据层 + * + * @author ruoyi + */ +public interface SysJobLogMapper +{ + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * @return 调度任务日志集合 + */ + public List selectJobLogList(SysJobLog jobLog); + + /** + * 查询所有调度任务日志 + * + * @return 调度任务日志列表 + */ + public List selectJobLogAll(); + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * @return 调度任务日志对象信息 + */ + public SysJobLog selectJobLogById(Long jobLogId); + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + * @return 结果 + */ + public int insertJobLog(SysJobLog jobLog); + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的数据ID + * @return 结果 + */ + public int deleteJobLogByIds(Long[] logIds); + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + * @return 结果 + */ + public int deleteJobLogById(Long jobId); + + /** + * 清空任务日志 + */ + public void cleanJobLog(); +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobMapper.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobMapper.java new file mode 100644 index 0000000..20f45db --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/mapper/SysJobMapper.java @@ -0,0 +1,67 @@ +package com.ruoyi.quartz.mapper; + +import java.util.List; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 调度任务信息 数据层 + * + * @author ruoyi + */ +public interface SysJobMapper +{ + /** + * 查询调度任务日志集合 + * + * @param job 调度信息 + * @return 操作日志集合 + */ + public List selectJobList(SysJob job); + + /** + * 查询所有调度任务 + * + * @return 调度任务列表 + */ + public List selectJobAll(); + + /** + * 通过调度ID查询调度任务信息 + * + * @param jobId 调度ID + * @return 角色对象信息 + */ + public SysJob selectJobById(Long jobId); + + /** + * 通过调度ID删除调度任务信息 + * + * @param jobId 调度ID + * @return 结果 + */ + public int deleteJobById(Long jobId); + + /** + * 批量删除调度任务信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteJobByIds(Long[] ids); + + /** + * 修改调度任务信息 + * + * @param job 调度任务信息 + * @return 结果 + */ + public int updateJob(SysJob job); + + /** + * 新增调度任务信息 + * + * @param job 调度任务信息 + * @return 结果 + */ + public int insertJob(SysJob job); +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobLogService.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobLogService.java new file mode 100644 index 0000000..8546792 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobLogService.java @@ -0,0 +1,56 @@ +package com.ruoyi.quartz.service; + +import java.util.List; +import com.ruoyi.quartz.domain.SysJobLog; + +/** + * 定时任务调度日志信息信息 服务层 + * + * @author ruoyi + */ +public interface ISysJobLogService +{ + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * @return 调度任务日志集合 + */ + public List selectJobLogList(SysJobLog jobLog); + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * @return 调度任务日志对象信息 + */ + public SysJobLog selectJobLogById(Long jobLogId); + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + */ + public void addJobLog(SysJobLog jobLog); + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的日志ID + * @return 结果 + */ + public int deleteJobLogByIds(Long[] logIds); + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + * @return 结果 + */ + public int deleteJobLogById(Long jobId); + + /** + * 清空任务日志 + */ + public void cleanJobLog(); +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobService.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobService.java new file mode 100644 index 0000000..437ade8 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/ISysJobService.java @@ -0,0 +1,102 @@ +package com.ruoyi.quartz.service; + +import java.util.List; +import org.quartz.SchedulerException; +import com.ruoyi.common.exception.job.TaskException; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 定时任务调度信息信息 服务层 + * + * @author ruoyi + */ +public interface ISysJobService +{ + /** + * 获取quartz调度器的计划任务 + * + * @param job 调度信息 + * @return 调度任务集合 + */ + public List selectJobList(SysJob job); + + /** + * 通过调度任务ID查询调度信息 + * + * @param jobId 调度任务ID + * @return 调度任务对象信息 + */ + public SysJob selectJobById(Long jobId); + + /** + * 暂停任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int pauseJob(SysJob job) throws SchedulerException; + + /** + * 恢复任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int resumeJob(SysJob job) throws SchedulerException; + + /** + * 删除任务后,所对应的trigger也将被删除 + * + * @param job 调度信息 + * @return 结果 + */ + public int deleteJob(SysJob job) throws SchedulerException; + + /** + * 批量删除调度信息 + * + * @param jobIds 需要删除的任务ID + * @return 结果 + */ + public void deleteJobByIds(Long[] jobIds) throws SchedulerException; + + /** + * 任务调度状态修改 + * + * @param job 调度信息 + * @return 结果 + */ + public int changeStatus(SysJob job) throws SchedulerException; + + /** + * 立即运行任务 + * + * @param job 调度信息 + * @return 结果 + */ + public boolean run(SysJob job) throws SchedulerException; + + /** + * 新增任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int insertJob(SysJob job) throws SchedulerException, TaskException; + + /** + * 更新任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int updateJob(SysJob job) throws SchedulerException, TaskException; + + /** + * 校验cron表达式是否有效 + * + * @param cronExpression 表达式 + * @return 结果 + */ + public boolean checkCronExpressionIsValid(String cronExpression); +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobLogServiceImpl.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobLogServiceImpl.java new file mode 100644 index 0000000..812eed7 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobLogServiceImpl.java @@ -0,0 +1,87 @@ +package com.ruoyi.quartz.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.quartz.domain.SysJobLog; +import com.ruoyi.quartz.mapper.SysJobLogMapper; +import com.ruoyi.quartz.service.ISysJobLogService; + +/** + * 定时任务调度日志信息 服务层 + * + * @author ruoyi + */ +@Service +public class SysJobLogServiceImpl implements ISysJobLogService +{ + @Autowired + private SysJobLogMapper jobLogMapper; + + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * @return 调度任务日志集合 + */ + @Override + public List selectJobLogList(SysJobLog jobLog) + { + return jobLogMapper.selectJobLogList(jobLog); + } + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * @return 调度任务日志对象信息 + */ + @Override + public SysJobLog selectJobLogById(Long jobLogId) + { + return jobLogMapper.selectJobLogById(jobLogId); + } + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + */ + @Override + public void addJobLog(SysJobLog jobLog) + { + jobLogMapper.insertJobLog(jobLog); + } + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的数据ID + * @return 结果 + */ + @Override + public int deleteJobLogByIds(Long[] logIds) + { + return jobLogMapper.deleteJobLogByIds(logIds); + } + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + */ + @Override + public int deleteJobLogById(Long jobId) + { + return jobLogMapper.deleteJobLogById(jobId); + } + + /** + * 清空任务日志 + */ + @Override + public void cleanJobLog() + { + jobLogMapper.cleanJobLog(); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobServiceImpl.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobServiceImpl.java new file mode 100644 index 0000000..77fdbb5 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/service/impl/SysJobServiceImpl.java @@ -0,0 +1,261 @@ +package com.ruoyi.quartz.service.impl; + +import java.util.List; +import javax.annotation.PostConstruct; +import org.quartz.JobDataMap; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.common.constant.ScheduleConstants; +import com.ruoyi.common.exception.job.TaskException; +import com.ruoyi.quartz.domain.SysJob; +import com.ruoyi.quartz.mapper.SysJobMapper; +import com.ruoyi.quartz.service.ISysJobService; +import com.ruoyi.quartz.util.CronUtils; +import com.ruoyi.quartz.util.ScheduleUtils; + +/** + * 定时任务调度信息 服务层 + * + * @author ruoyi + */ +@Service +public class SysJobServiceImpl implements ISysJobService +{ + @Autowired + private Scheduler scheduler; + + @Autowired + private SysJobMapper jobMapper; + + /** + * 项目启动时,初始化定时器 主要是防止手动修改数据库导致未同步到定时任务处理(注:不能手动修改数据库ID和任务组名,否则会导致脏数据) + */ + @PostConstruct + public void init() throws SchedulerException, TaskException + { + scheduler.clear(); + List jobList = jobMapper.selectJobAll(); + for (SysJob job : jobList) + { + ScheduleUtils.createScheduleJob(scheduler, job); + } + } + + /** + * 获取quartz调度器的计划任务列表 + * + * @param job 调度信息 + * @return + */ + @Override + public List selectJobList(SysJob job) + { + return jobMapper.selectJobList(job); + } + + /** + * 通过调度任务ID查询调度信息 + * + * @param jobId 调度任务ID + * @return 调度任务对象信息 + */ + @Override + public SysJob selectJobById(Long jobId) + { + return jobMapper.selectJobById(jobId); + } + + /** + * 暂停任务 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int pauseJob(SysJob job) throws SchedulerException + { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + job.setStatus(ScheduleConstants.Status.PAUSE.getValue()); + int rows = jobMapper.updateJob(job); + if (rows > 0) + { + scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 恢复任务 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int resumeJob(SysJob job) throws SchedulerException + { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + job.setStatus(ScheduleConstants.Status.NORMAL.getValue()); + int rows = jobMapper.updateJob(job); + if (rows > 0) + { + scheduler.resumeJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 删除任务后,所对应的trigger也将被删除 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteJob(SysJob job) throws SchedulerException + { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + int rows = jobMapper.deleteJobById(jobId); + if (rows > 0) + { + scheduler.deleteJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 批量删除调度信息 + * + * @param jobIds 需要删除的任务ID + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteJobByIds(Long[] jobIds) throws SchedulerException + { + for (Long jobId : jobIds) + { + SysJob job = jobMapper.selectJobById(jobId); + deleteJob(job); + } + } + + /** + * 任务调度状态修改 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int changeStatus(SysJob job) throws SchedulerException + { + int rows = 0; + String status = job.getStatus(); + if (ScheduleConstants.Status.NORMAL.getValue().equals(status)) + { + rows = resumeJob(job); + } + else if (ScheduleConstants.Status.PAUSE.getValue().equals(status)) + { + rows = pauseJob(job); + } + return rows; + } + + /** + * 立即运行任务 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean run(SysJob job) throws SchedulerException + { + boolean result = false; + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + SysJob properties = selectJobById(job.getJobId()); + // 参数 + JobDataMap dataMap = new JobDataMap(); + dataMap.put(ScheduleConstants.TASK_PROPERTIES, properties); + JobKey jobKey = ScheduleUtils.getJobKey(jobId, jobGroup); + if (scheduler.checkExists(jobKey)) + { + result = true; + scheduler.triggerJob(jobKey, dataMap); + } + return result; + } + + /** + * 新增任务 + * + * @param job 调度信息 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int insertJob(SysJob job) throws SchedulerException, TaskException + { + job.setStatus(ScheduleConstants.Status.PAUSE.getValue()); + int rows = jobMapper.insertJob(job); + if (rows > 0) + { + ScheduleUtils.createScheduleJob(scheduler, job); + } + return rows; + } + + /** + * 更新任务的时间表达式 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int updateJob(SysJob job) throws SchedulerException, TaskException + { + SysJob properties = selectJobById(job.getJobId()); + int rows = jobMapper.updateJob(job); + if (rows > 0) + { + updateSchedulerJob(job, properties.getJobGroup()); + } + return rows; + } + + /** + * 更新任务 + * + * @param job 任务对象 + * @param jobGroup 任务组名 + */ + public void updateSchedulerJob(SysJob job, String jobGroup) throws SchedulerException, TaskException + { + Long jobId = job.getJobId(); + // 判断是否存在 + JobKey jobKey = ScheduleUtils.getJobKey(jobId, jobGroup); + if (scheduler.checkExists(jobKey)) + { + // 防止创建时存在数据问题 先移除,然后在执行创建操作 + scheduler.deleteJob(jobKey); + } + ScheduleUtils.createScheduleJob(scheduler, job); + } + + /** + * 校验cron表达式是否有效 + * + * @param cronExpression 表达式 + * @return 结果 + */ + @Override + public boolean checkCronExpressionIsValid(String cronExpression) + { + return CronUtils.isValid(cronExpression); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java new file mode 100644 index 0000000..853243b --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/RyTask.java @@ -0,0 +1,28 @@ +package com.ruoyi.quartz.task; + +import org.springframework.stereotype.Component; +import com.ruoyi.common.utils.StringUtils; + +/** + * 定时任务调度测试 + * + * @author ruoyi + */ +@Component("ryTask") +public class RyTask +{ + public void ryMultipleParams(String s, Boolean b, Long l, Double d, Integer i) + { + System.out.println(StringUtils.format("执行多参方法: 字符串类型{},布尔类型{},长整型{},浮点型{},整形{}", s, b, l, d, i)); + } + + public void ryParams(String params) + { + System.out.println("执行有参方法:" + params); + } + + public void ryNoParams() + { + System.out.println("执行无参方法"); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/AbstractQuartzJob.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/AbstractQuartzJob.java new file mode 100644 index 0000000..eec1faf --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/AbstractQuartzJob.java @@ -0,0 +1,106 @@ +package com.ruoyi.quartz.util; + +import java.util.Date; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.ScheduleConstants; +import com.ruoyi.common.utils.ExceptionUtil; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.bean.BeanUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.quartz.domain.SysJob; +import com.ruoyi.quartz.domain.SysJobLog; +import com.ruoyi.quartz.service.ISysJobLogService; + +/** + * 抽象quartz调用 + * + * @author ruoyi + */ +public abstract class AbstractQuartzJob implements Job +{ + private static final Logger log = LoggerFactory.getLogger(AbstractQuartzJob.class); + + /** + * 线程本地变量 + */ + private static ThreadLocal threadLocal = new ThreadLocal<>(); + + @Override + public void execute(JobExecutionContext context) + { + SysJob sysJob = new SysJob(); + BeanUtils.copyBeanProp(sysJob, context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES)); + try + { + before(context, sysJob); + if (sysJob != null) + { + doExecute(context, sysJob); + } + after(context, sysJob, null); + } + catch (Exception e) + { + log.error("任务执行异常 - :", e); + after(context, sysJob, e); + } + } + + /** + * 执行前 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + */ + protected void before(JobExecutionContext context, SysJob sysJob) + { + threadLocal.set(new Date()); + } + + /** + * 执行后 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + */ + protected void after(JobExecutionContext context, SysJob sysJob, Exception e) + { + Date startTime = threadLocal.get(); + threadLocal.remove(); + + final SysJobLog sysJobLog = new SysJobLog(); + sysJobLog.setJobName(sysJob.getJobName()); + sysJobLog.setJobGroup(sysJob.getJobGroup()); + sysJobLog.setInvokeTarget(sysJob.getInvokeTarget()); + sysJobLog.setStartTime(startTime); + sysJobLog.setStopTime(new Date()); + long runMs = sysJobLog.getStopTime().getTime() - sysJobLog.getStartTime().getTime(); + sysJobLog.setJobMessage(sysJobLog.getJobName() + " 总共耗时:" + runMs + "毫秒"); + if (e != null) + { + sysJobLog.setStatus(Constants.FAIL); + String errorMsg = StringUtils.substring(ExceptionUtil.getExceptionMessage(e), 0, 2000); + sysJobLog.setExceptionInfo(errorMsg); + } + else + { + sysJobLog.setStatus(Constants.SUCCESS); + } + + // 写入数据库当中 + SpringUtils.getBean(ISysJobLogService.class).addJobLog(sysJobLog); + } + + /** + * 执行方法,由子类重载 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + * @throws Exception 执行过程中的异常 + */ + protected abstract void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception; +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/CronUtils.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/CronUtils.java new file mode 100644 index 0000000..dd53839 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/CronUtils.java @@ -0,0 +1,63 @@ +package com.ruoyi.quartz.util; + +import java.text.ParseException; +import java.util.Date; +import org.quartz.CronExpression; + +/** + * cron表达式工具类 + * + * @author ruoyi + * + */ +public class CronUtils +{ + /** + * 返回一个布尔值代表一个给定的Cron表达式的有效性 + * + * @param cronExpression Cron表达式 + * @return boolean 表达式是否有效 + */ + public static boolean isValid(String cronExpression) + { + return CronExpression.isValidExpression(cronExpression); + } + + /** + * 返回一个字符串值,表示该消息无效Cron表达式给出有效性 + * + * @param cronExpression Cron表达式 + * @return String 无效时返回表达式错误描述,如果有效返回null + */ + public static String getInvalidMessage(String cronExpression) + { + try + { + new CronExpression(cronExpression); + return null; + } + catch (ParseException pe) + { + return pe.getMessage(); + } + } + + /** + * 返回下一个执行时间根据给定的Cron表达式 + * + * @param cronExpression Cron表达式 + * @return Date 下次Cron表达式执行时间 + */ + public static Date getNextExecution(String cronExpression) + { + try + { + CronExpression cron = new CronExpression(cronExpression); + return cron.getNextValidTimeAfter(new Date(System.currentTimeMillis())); + } + catch (ParseException e) + { + throw new IllegalArgumentException(e.getMessage()); + } + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/JobInvokeUtil.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/JobInvokeUtil.java new file mode 100644 index 0000000..dea8cde --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/JobInvokeUtil.java @@ -0,0 +1,182 @@ +package com.ruoyi.quartz.util; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.LinkedList; +import java.util.List; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 任务执行工具 + * + * @author ruoyi + */ +public class JobInvokeUtil +{ + /** + * 执行方法 + * + * @param sysJob 系统任务 + */ + public static void invokeMethod(SysJob sysJob) throws Exception + { + String invokeTarget = sysJob.getInvokeTarget(); + String beanName = getBeanName(invokeTarget); + String methodName = getMethodName(invokeTarget); + List methodParams = getMethodParams(invokeTarget); + + if (!isValidClassName(beanName)) + { + Object bean = SpringUtils.getBean(beanName); + invokeMethod(bean, methodName, methodParams); + } + else + { + Object bean = Class.forName(beanName).getDeclaredConstructor().newInstance(); + invokeMethod(bean, methodName, methodParams); + } + } + + /** + * 调用任务方法 + * + * @param bean 目标对象 + * @param methodName 方法名称 + * @param methodParams 方法参数 + */ + private static void invokeMethod(Object bean, String methodName, List methodParams) + throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, + InvocationTargetException + { + if (StringUtils.isNotNull(methodParams) && methodParams.size() > 0) + { + Method method = bean.getClass().getMethod(methodName, getMethodParamsType(methodParams)); + method.invoke(bean, getMethodParamsValue(methodParams)); + } + else + { + Method method = bean.getClass().getMethod(methodName); + method.invoke(bean); + } + } + + /** + * 校验是否为为class包名 + * + * @param invokeTarget 名称 + * @return true是 false否 + */ + public static boolean isValidClassName(String invokeTarget) + { + return StringUtils.countMatches(invokeTarget, ".") > 1; + } + + /** + * 获取bean名称 + * + * @param invokeTarget 目标字符串 + * @return bean名称 + */ + public static String getBeanName(String invokeTarget) + { + String beanName = StringUtils.substringBefore(invokeTarget, "("); + return StringUtils.substringBeforeLast(beanName, "."); + } + + /** + * 获取bean方法 + * + * @param invokeTarget 目标字符串 + * @return method方法 + */ + public static String getMethodName(String invokeTarget) + { + String methodName = StringUtils.substringBefore(invokeTarget, "("); + return StringUtils.substringAfterLast(methodName, "."); + } + + /** + * 获取method方法参数相关列表 + * + * @param invokeTarget 目标字符串 + * @return method方法相关参数列表 + */ + public static List getMethodParams(String invokeTarget) + { + String methodStr = StringUtils.substringBetweenLast(invokeTarget, "(", ")"); + if (StringUtils.isEmpty(methodStr)) + { + return null; + } + String[] methodParams = methodStr.split(",(?=([^\"']*[\"'][^\"']*[\"'])*[^\"']*$)"); + List classs = new LinkedList<>(); + for (int i = 0; i < methodParams.length; i++) + { + String str = StringUtils.trimToEmpty(methodParams[i]); + // String字符串类型,以'或"开头 + if (StringUtils.startsWithAny(str, "'", "\"")) + { + classs.add(new Object[] { StringUtils.substring(str, 1, str.length() - 1), String.class }); + } + // boolean布尔类型,等于true或者false + else if ("true".equalsIgnoreCase(str) || "false".equalsIgnoreCase(str)) + { + classs.add(new Object[] { Boolean.valueOf(str), Boolean.class }); + } + // long长整形,以L结尾 + else if (StringUtils.endsWith(str, "L")) + { + classs.add(new Object[] { Long.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Long.class }); + } + // double浮点类型,以D结尾 + else if (StringUtils.endsWith(str, "D")) + { + classs.add(new Object[] { Double.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Double.class }); + } + // 其他类型归类为整形 + else + { + classs.add(new Object[] { Integer.valueOf(str), Integer.class }); + } + } + return classs; + } + + /** + * 获取参数类型 + * + * @param methodParams 参数相关列表 + * @return 参数类型列表 + */ + public static Class[] getMethodParamsType(List methodParams) + { + Class[] classs = new Class[methodParams.size()]; + int index = 0; + for (Object[] os : methodParams) + { + classs[index] = (Class) os[1]; + index++; + } + return classs; + } + + /** + * 获取参数值 + * + * @param methodParams 参数相关列表 + * @return 参数值列表 + */ + public static Object[] getMethodParamsValue(List methodParams) + { + Object[] classs = new Object[methodParams.size()]; + int index = 0; + for (Object[] os : methodParams) + { + classs[index] = (Object) os[0]; + index++; + } + return classs; + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzDisallowConcurrentExecution.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzDisallowConcurrentExecution.java new file mode 100644 index 0000000..5e13558 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzDisallowConcurrentExecution.java @@ -0,0 +1,21 @@ +package com.ruoyi.quartz.util; + +import org.quartz.DisallowConcurrentExecution; +import org.quartz.JobExecutionContext; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 定时任务处理(禁止并发执行) + * + * @author ruoyi + * + */ +@DisallowConcurrentExecution +public class QuartzDisallowConcurrentExecution extends AbstractQuartzJob +{ + @Override + protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception + { + JobInvokeUtil.invokeMethod(sysJob); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzJobExecution.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzJobExecution.java new file mode 100644 index 0000000..e975326 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/QuartzJobExecution.java @@ -0,0 +1,19 @@ +package com.ruoyi.quartz.util; + +import org.quartz.JobExecutionContext; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 定时任务处理(允许并发执行) + * + * @author ruoyi + * + */ +public class QuartzJobExecution extends AbstractQuartzJob +{ + @Override + protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception + { + JobInvokeUtil.invokeMethod(sysJob); + } +} diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/ScheduleUtils.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/ScheduleUtils.java new file mode 100644 index 0000000..5a83f50 --- /dev/null +++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/ScheduleUtils.java @@ -0,0 +1,141 @@ +package com.ruoyi.quartz.util; + +import org.quartz.CronScheduleBuilder; +import org.quartz.CronTrigger; +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.TriggerBuilder; +import org.quartz.TriggerKey; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.ScheduleConstants; +import com.ruoyi.common.exception.job.TaskException; +import com.ruoyi.common.exception.job.TaskException.Code; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.quartz.domain.SysJob; + +/** + * 定时任务工具类 + * + * @author ruoyi + * + */ +public class ScheduleUtils +{ + /** + * 得到quartz任务类 + * + * @param sysJob 执行计划 + * @return 具体执行任务类 + */ + private static Class getQuartzJobClass(SysJob sysJob) + { + boolean isConcurrent = "0".equals(sysJob.getConcurrent()); + return isConcurrent ? QuartzJobExecution.class : QuartzDisallowConcurrentExecution.class; + } + + /** + * 构建任务触发对象 + */ + public static TriggerKey getTriggerKey(Long jobId, String jobGroup) + { + return TriggerKey.triggerKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup); + } + + /** + * 构建任务键对象 + */ + public static JobKey getJobKey(Long jobId, String jobGroup) + { + return JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup); + } + + /** + * 创建定时任务 + */ + public static void createScheduleJob(Scheduler scheduler, SysJob job) throws SchedulerException, TaskException + { + Class jobClass = getQuartzJobClass(job); + // 构建job信息 + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(getJobKey(jobId, jobGroup)).build(); + + // 表达式调度构建器 + CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression()); + cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder); + + // 按新的cronExpression表达式构建一个新的trigger + CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(jobId, jobGroup)) + .withSchedule(cronScheduleBuilder).build(); + + // 放入参数,运行时的方法可以获取 + jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job); + + // 判断是否存在 + if (scheduler.checkExists(getJobKey(jobId, jobGroup))) + { + // 防止创建时存在数据问题 先移除,然后在执行创建操作 + scheduler.deleteJob(getJobKey(jobId, jobGroup)); + } + + // 判断任务是否过期 + if (StringUtils.isNotNull(CronUtils.getNextExecution(job.getCronExpression()))) + { + // 执行调度任务 + scheduler.scheduleJob(jobDetail, trigger); + } + + // 暂停任务 + if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue())) + { + scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + } + + /** + * 设置定时任务策略 + */ + public static CronScheduleBuilder handleCronScheduleMisfirePolicy(SysJob job, CronScheduleBuilder cb) + throws TaskException + { + switch (job.getMisfirePolicy()) + { + case ScheduleConstants.MISFIRE_DEFAULT: + return cb; + case ScheduleConstants.MISFIRE_IGNORE_MISFIRES: + return cb.withMisfireHandlingInstructionIgnoreMisfires(); + case ScheduleConstants.MISFIRE_FIRE_AND_PROCEED: + return cb.withMisfireHandlingInstructionFireAndProceed(); + case ScheduleConstants.MISFIRE_DO_NOTHING: + return cb.withMisfireHandlingInstructionDoNothing(); + default: + throw new TaskException("The task misfire policy '" + job.getMisfirePolicy() + + "' cannot be used in cron schedule tasks", Code.CONFIG_ERROR); + } + } + + /** + * 检查包名是否为白名单配置 + * + * @param invokeTarget 目标字符串 + * @return 结果 + */ + public static boolean whiteList(String invokeTarget) + { + String packageName = StringUtils.substringBefore(invokeTarget, "("); + int count = StringUtils.countMatches(packageName, "."); + if (count > 1) + { + return StringUtils.startsWithAny(invokeTarget, Constants.JOB_WHITELIST_STR); + } + Object obj = SpringUtils.getBean(StringUtils.split(invokeTarget, ".")[0]); + String beanPackageName = obj.getClass().getPackage().getName(); + return StringUtils.startsWithAny(beanPackageName, Constants.JOB_WHITELIST_STR) + && !StringUtils.startsWithAny(beanPackageName, Constants.JOB_ERROR_STR); + } +} diff --git a/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml b/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml new file mode 100644 index 0000000..ba1b683 --- /dev/null +++ b/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + select job_log_id, job_name, job_group, invoke_target, job_message, status, exception_info, create_time + from sys_job_log + + + + + + + + + + delete from sys_job_log where job_log_id = #{jobLogId} + + + + delete from sys_job_log where job_log_id in + + #{jobLogId} + + + + + truncate table sys_job_log + + + + insert into sys_job_log( + job_log_id, + job_name, + job_group, + invoke_target, + job_message, + status, + exception_info, + create_time + )values( + #{jobLogId}, + #{jobName}, + #{jobGroup}, + #{invokeTarget}, + #{jobMessage}, + #{status}, + #{exceptionInfo}, + sysdate() + ) + + + \ No newline at end of file diff --git a/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml b/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml new file mode 100644 index 0000000..5605c44 --- /dev/null +++ b/ruoyi-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + select job_id, job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark + from sys_job + + + + + + + + + + delete from sys_job where job_id = #{jobId} + + + + delete from sys_job where job_id in + + #{jobId} + + + + + update sys_job + + job_name = #{jobName}, + job_group = #{jobGroup}, + invoke_target = #{invokeTarget}, + cron_expression = #{cronExpression}, + misfire_policy = #{misfirePolicy}, + concurrent = #{concurrent}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where job_id = #{jobId} + + + + insert into sys_job( + job_id, + job_name, + job_group, + invoke_target, + cron_expression, + misfire_policy, + concurrent, + status, + remark, + create_by, + create_time + )values( + #{jobId}, + #{jobName}, + #{jobGroup}, + #{invokeTarget}, + #{cronExpression}, + #{misfirePolicy}, + #{concurrent}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + \ No newline at end of file diff --git a/ruoyi-system/pom.xml b/ruoyi-system/pom.xml new file mode 100644 index 0000000..f514d3d --- /dev/null +++ b/ruoyi-system/pom.xml @@ -0,0 +1,28 @@ + + + + ruoyi + com.ruoyi + 3.9.0 + + 4.0.0 + + ruoyi-system + + + system系统模块 + + + + + + + com.ruoyi + ruoyi-common + + + + + \ No newline at end of file diff --git a/ruoyi-system/ruoyi-system.iml b/ruoyi-system/ruoyi-system.iml new file mode 100644 index 0000000..662ed02 --- /dev/null +++ b/ruoyi-system/ruoyi-system.iml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysCache.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysCache.java new file mode 100644 index 0000000..83f0703 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysCache.java @@ -0,0 +1,81 @@ +package com.ruoyi.system.domain; + +import com.ruoyi.common.utils.StringUtils; + +/** + * 缓存信息 + * + * @author ruoyi + */ +public class SysCache +{ + /** 缓存名称 */ + private String cacheName = ""; + + /** 缓存键名 */ + private String cacheKey = ""; + + /** 缓存内容 */ + private String cacheValue = ""; + + /** 备注 */ + private String remark = ""; + + public SysCache() + { + + } + + public SysCache(String cacheName, String remark) + { + this.cacheName = cacheName; + this.remark = remark; + } + + public SysCache(String cacheName, String cacheKey, String cacheValue) + { + this.cacheName = StringUtils.replace(cacheName, ":", ""); + this.cacheKey = StringUtils.replace(cacheKey, cacheName, ""); + this.cacheValue = cacheValue; + } + + public String getCacheName() + { + return cacheName; + } + + public void setCacheName(String cacheName) + { + this.cacheName = cacheName; + } + + public String getCacheKey() + { + return cacheKey; + } + + public void setCacheKey(String cacheKey) + { + this.cacheKey = cacheKey; + } + + public String getCacheValue() + { + return cacheValue; + } + + public void setCacheValue(String cacheValue) + { + this.cacheValue = cacheValue; + } + + public String getRemark() + { + return remark; + } + + public void setRemark(String remark) + { + this.remark = remark; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysConfig.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysConfig.java new file mode 100644 index 0000000..c54678c --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysConfig.java @@ -0,0 +1,111 @@ +package com.ruoyi.system.domain; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 参数配置表 sys_config + * + * @author ruoyi + */ +public class SysConfig extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 参数主键 */ + @Excel(name = "参数主键", cellType = ColumnType.NUMERIC) + private Long configId; + + /** 参数名称 */ + @Excel(name = "参数名称") + private String configName; + + /** 参数键名 */ + @Excel(name = "参数键名") + private String configKey; + + /** 参数键值 */ + @Excel(name = "参数键值") + private String configValue; + + /** 系统内置(Y是 N否) */ + @Excel(name = "系统内置", readConverterExp = "Y=是,N=否") + private String configType; + + public Long getConfigId() + { + return configId; + } + + public void setConfigId(Long configId) + { + this.configId = configId; + } + + @NotBlank(message = "参数名称不能为空") + @Size(min = 0, max = 100, message = "参数名称不能超过100个字符") + public String getConfigName() + { + return configName; + } + + public void setConfigName(String configName) + { + this.configName = configName; + } + + @NotBlank(message = "参数键名长度不能为空") + @Size(min = 0, max = 100, message = "参数键名长度不能超过100个字符") + public String getConfigKey() + { + return configKey; + } + + public void setConfigKey(String configKey) + { + this.configKey = configKey; + } + + @NotBlank(message = "参数键值不能为空") + @Size(min = 0, max = 500, message = "参数键值长度不能超过500个字符") + public String getConfigValue() + { + return configValue; + } + + public void setConfigValue(String configValue) + { + this.configValue = configValue; + } + + public String getConfigType() + { + return configType; + } + + public void setConfigType(String configType) + { + this.configType = configType; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("configId", getConfigId()) + .append("configName", getConfigName()) + .append("configKey", getConfigKey()) + .append("configValue", getConfigValue()) + .append("configType", getConfigType()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysLogininfor.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysLogininfor.java new file mode 100644 index 0000000..7fdea30 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysLogininfor.java @@ -0,0 +1,144 @@ +package com.ruoyi.system.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 系统访问记录表 sys_logininfor + * + * @author ruoyi + */ +public class SysLogininfor extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** ID */ + @Excel(name = "序号", cellType = ColumnType.NUMERIC) + private Long infoId; + + /** 用户账号 */ + @Excel(name = "用户账号") + private String userName; + + /** 登录状态 0成功 1失败 */ + @Excel(name = "登录状态", readConverterExp = "0=成功,1=失败") + private String status; + + /** 登录IP地址 */ + @Excel(name = "登录地址") + private String ipaddr; + + /** 登录地点 */ + @Excel(name = "登录地点") + private String loginLocation; + + /** 浏览器类型 */ + @Excel(name = "浏览器") + private String browser; + + /** 操作系统 */ + @Excel(name = "操作系统") + private String os; + + /** 提示消息 */ + @Excel(name = "提示消息") + private String msg; + + /** 访问时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "访问时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date loginTime; + + public Long getInfoId() + { + return infoId; + } + + public void setInfoId(Long infoId) + { + this.infoId = infoId; + } + + public String getUserName() + { + return userName; + } + + public void setUserName(String userName) + { + this.userName = userName; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getIpaddr() + { + return ipaddr; + } + + public void setIpaddr(String ipaddr) + { + this.ipaddr = ipaddr; + } + + public String getLoginLocation() + { + return loginLocation; + } + + public void setLoginLocation(String loginLocation) + { + this.loginLocation = loginLocation; + } + + public String getBrowser() + { + return browser; + } + + public void setBrowser(String browser) + { + this.browser = browser; + } + + public String getOs() + { + return os; + } + + public void setOs(String os) + { + this.os = os; + } + + public String getMsg() + { + return msg; + } + + public void setMsg(String msg) + { + this.msg = msg; + } + + public Date getLoginTime() + { + return loginTime; + } + + public void setLoginTime(Date loginTime) + { + this.loginTime = loginTime; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysNotice.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysNotice.java new file mode 100644 index 0000000..8c07a54 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysNotice.java @@ -0,0 +1,102 @@ +package com.ruoyi.system.domain; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.xss.Xss; + +/** + * 通知公告表 sys_notice + * + * @author ruoyi + */ +public class SysNotice extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 公告ID */ + private Long noticeId; + + /** 公告标题 */ + private String noticeTitle; + + /** 公告类型(1通知 2公告) */ + private String noticeType; + + /** 公告内容 */ + private String noticeContent; + + /** 公告状态(0正常 1关闭) */ + private String status; + + public Long getNoticeId() + { + return noticeId; + } + + public void setNoticeId(Long noticeId) + { + this.noticeId = noticeId; + } + + public void setNoticeTitle(String noticeTitle) + { + this.noticeTitle = noticeTitle; + } + + @Xss(message = "公告标题不能包含脚本字符") + @NotBlank(message = "公告标题不能为空") + @Size(min = 0, max = 50, message = "公告标题不能超过50个字符") + public String getNoticeTitle() + { + return noticeTitle; + } + + public void setNoticeType(String noticeType) + { + this.noticeType = noticeType; + } + + public String getNoticeType() + { + return noticeType; + } + + public void setNoticeContent(String noticeContent) + { + this.noticeContent = noticeContent; + } + + public String getNoticeContent() + { + return noticeContent; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getStatus() + { + return status; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("noticeId", getNoticeId()) + .append("noticeTitle", getNoticeTitle()) + .append("noticeType", getNoticeType()) + .append("noticeContent", getNoticeContent()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOperLog.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOperLog.java new file mode 100644 index 0000000..f6761df --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOperLog.java @@ -0,0 +1,269 @@ +package com.ruoyi.system.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 操作日志记录表 oper_log + * + * @author ruoyi + */ +public class SysOperLog extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 日志主键 */ + @Excel(name = "操作序号", cellType = ColumnType.NUMERIC) + private Long operId; + + /** 操作模块 */ + @Excel(name = "操作模块") + private String title; + + /** 业务类型(0其它 1新增 2修改 3删除) */ + @Excel(name = "业务类型", readConverterExp = "0=其它,1=新增,2=修改,3=删除,4=授权,5=导出,6=导入,7=强退,8=生成代码,9=清空数据") + private Integer businessType; + + /** 业务类型数组 */ + private Integer[] businessTypes; + + /** 请求方法 */ + @Excel(name = "请求方法") + private String method; + + /** 请求方式 */ + @Excel(name = "请求方式") + private String requestMethod; + + /** 操作类别(0其它 1后台用户 2手机端用户) */ + @Excel(name = "操作类别", readConverterExp = "0=其它,1=后台用户,2=手机端用户") + private Integer operatorType; + + /** 操作人员 */ + @Excel(name = "操作人员") + private String operName; + + /** 部门名称 */ + @Excel(name = "部门名称") + private String deptName; + + /** 请求url */ + @Excel(name = "请求地址") + private String operUrl; + + /** 操作地址 */ + @Excel(name = "操作地址") + private String operIp; + + /** 操作地点 */ + @Excel(name = "操作地点") + private String operLocation; + + /** 请求参数 */ + @Excel(name = "请求参数") + private String operParam; + + /** 返回参数 */ + @Excel(name = "返回参数") + private String jsonResult; + + /** 操作状态(0正常 1异常) */ + @Excel(name = "状态", readConverterExp = "0=正常,1=异常") + private Integer status; + + /** 错误消息 */ + @Excel(name = "错误消息") + private String errorMsg; + + /** 操作时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "操作时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date operTime; + + /** 消耗时间 */ + @Excel(name = "消耗时间", suffix = "毫秒") + private Long costTime; + + public Long getOperId() + { + return operId; + } + + public void setOperId(Long operId) + { + this.operId = operId; + } + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public Integer getBusinessType() + { + return businessType; + } + + public void setBusinessType(Integer businessType) + { + this.businessType = businessType; + } + + public Integer[] getBusinessTypes() + { + return businessTypes; + } + + public void setBusinessTypes(Integer[] businessTypes) + { + this.businessTypes = businessTypes; + } + + public String getMethod() + { + return method; + } + + public void setMethod(String method) + { + this.method = method; + } + + public String getRequestMethod() + { + return requestMethod; + } + + public void setRequestMethod(String requestMethod) + { + this.requestMethod = requestMethod; + } + + public Integer getOperatorType() + { + return operatorType; + } + + public void setOperatorType(Integer operatorType) + { + this.operatorType = operatorType; + } + + public String getOperName() + { + return operName; + } + + public void setOperName(String operName) + { + this.operName = operName; + } + + public String getDeptName() + { + return deptName; + } + + public void setDeptName(String deptName) + { + this.deptName = deptName; + } + + public String getOperUrl() + { + return operUrl; + } + + public void setOperUrl(String operUrl) + { + this.operUrl = operUrl; + } + + public String getOperIp() + { + return operIp; + } + + public void setOperIp(String operIp) + { + this.operIp = operIp; + } + + public String getOperLocation() + { + return operLocation; + } + + public void setOperLocation(String operLocation) + { + this.operLocation = operLocation; + } + + public String getOperParam() + { + return operParam; + } + + public void setOperParam(String operParam) + { + this.operParam = operParam; + } + + public String getJsonResult() + { + return jsonResult; + } + + public void setJsonResult(String jsonResult) + { + this.jsonResult = jsonResult; + } + + public Integer getStatus() + { + return status; + } + + public void setStatus(Integer status) + { + this.status = status; + } + + public String getErrorMsg() + { + return errorMsg; + } + + public void setErrorMsg(String errorMsg) + { + this.errorMsg = errorMsg; + } + + public Date getOperTime() + { + return operTime; + } + + public void setOperTime(Date operTime) + { + this.operTime = operTime; + } + + public Long getCostTime() + { + return costTime; + } + + public void setCostTime(Long costTime) + { + this.costTime = costTime; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysPost.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysPost.java new file mode 100644 index 0000000..820a13b --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysPost.java @@ -0,0 +1,124 @@ +package com.ruoyi.system.domain; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 岗位表 sys_post + * + * @author ruoyi + */ +public class SysPost extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 岗位序号 */ + @Excel(name = "岗位序号", cellType = ColumnType.NUMERIC) + private Long postId; + + /** 岗位编码 */ + @Excel(name = "岗位编码") + private String postCode; + + /** 岗位名称 */ + @Excel(name = "岗位名称") + private String postName; + + /** 岗位排序 */ + @Excel(name = "岗位排序") + private Integer postSort; + + /** 状态(0正常 1停用) */ + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** 用户是否存在此岗位标识 默认不存在 */ + private boolean flag = false; + + public Long getPostId() + { + return postId; + } + + public void setPostId(Long postId) + { + this.postId = postId; + } + + @NotBlank(message = "岗位编码不能为空") + @Size(min = 0, max = 64, message = "岗位编码长度不能超过64个字符") + public String getPostCode() + { + return postCode; + } + + public void setPostCode(String postCode) + { + this.postCode = postCode; + } + + @NotBlank(message = "岗位名称不能为空") + @Size(min = 0, max = 50, message = "岗位名称长度不能超过50个字符") + public String getPostName() + { + return postName; + } + + public void setPostName(String postName) + { + this.postName = postName; + } + + @NotNull(message = "显示顺序不能为空") + public Integer getPostSort() + { + return postSort; + } + + public void setPostSort(Integer postSort) + { + this.postSort = postSort; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public boolean isFlag() + { + return flag; + } + + public void setFlag(boolean flag) + { + this.flag = flag; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("postId", getPostId()) + .append("postCode", getPostCode()) + .append("postName", getPostName()) + .append("postSort", getPostSort()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleDept.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleDept.java new file mode 100644 index 0000000..47b21bf --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleDept.java @@ -0,0 +1,46 @@ +package com.ruoyi.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 角色和部门关联 sys_role_dept + * + * @author ruoyi + */ +public class SysRoleDept +{ + /** 角色ID */ + private Long roleId; + + /** 部门ID */ + private Long deptId; + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + public Long getDeptId() + { + return deptId; + } + + public void setDeptId(Long deptId) + { + this.deptId = deptId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("roleId", getRoleId()) + .append("deptId", getDeptId()) + .toString(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleMenu.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleMenu.java new file mode 100644 index 0000000..de10a74 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysRoleMenu.java @@ -0,0 +1,46 @@ +package com.ruoyi.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 角色和菜单关联 sys_role_menu + * + * @author ruoyi + */ +public class SysRoleMenu +{ + /** 角色ID */ + private Long roleId; + + /** 菜单ID */ + private Long menuId; + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + public Long getMenuId() + { + return menuId; + } + + public void setMenuId(Long menuId) + { + this.menuId = menuId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("roleId", getRoleId()) + .append("menuId", getMenuId()) + .toString(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserOnline.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserOnline.java new file mode 100644 index 0000000..2bbd318 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserOnline.java @@ -0,0 +1,113 @@ +package com.ruoyi.system.domain; + +/** + * 当前在线会话 + * + * @author ruoyi + */ +public class SysUserOnline +{ + /** 会话编号 */ + private String tokenId; + + /** 部门名称 */ + private String deptName; + + /** 用户名称 */ + private String userName; + + /** 登录IP地址 */ + private String ipaddr; + + /** 登录地址 */ + private String loginLocation; + + /** 浏览器类型 */ + private String browser; + + /** 操作系统 */ + private String os; + + /** 登录时间 */ + private Long loginTime; + + public String getTokenId() + { + return tokenId; + } + + public void setTokenId(String tokenId) + { + this.tokenId = tokenId; + } + + public String getDeptName() + { + return deptName; + } + + public void setDeptName(String deptName) + { + this.deptName = deptName; + } + + public String getUserName() + { + return userName; + } + + public void setUserName(String userName) + { + this.userName = userName; + } + + public String getIpaddr() + { + return ipaddr; + } + + public void setIpaddr(String ipaddr) + { + this.ipaddr = ipaddr; + } + + public String getLoginLocation() + { + return loginLocation; + } + + public void setLoginLocation(String loginLocation) + { + this.loginLocation = loginLocation; + } + + public String getBrowser() + { + return browser; + } + + public void setBrowser(String browser) + { + this.browser = browser; + } + + public String getOs() + { + return os; + } + + public void setOs(String os) + { + this.os = os; + } + + public Long getLoginTime() + { + return loginTime; + } + + public void setLoginTime(Long loginTime) + { + this.loginTime = loginTime; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserPost.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserPost.java new file mode 100644 index 0000000..6e8c416 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserPost.java @@ -0,0 +1,46 @@ +package com.ruoyi.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 用户和岗位关联 sys_user_post + * + * @author ruoyi + */ +public class SysUserPost +{ + /** 用户ID */ + private Long userId; + + /** 岗位ID */ + private Long postId; + + public Long getUserId() + { + return userId; + } + + public void setUserId(Long userId) + { + this.userId = userId; + } + + public Long getPostId() + { + return postId; + } + + public void setPostId(Long postId) + { + this.postId = postId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("userId", getUserId()) + .append("postId", getPostId()) + .toString(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserRole.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserRole.java new file mode 100644 index 0000000..4d15810 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUserRole.java @@ -0,0 +1,46 @@ +package com.ruoyi.system.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 用户和角色关联 sys_user_role + * + * @author ruoyi + */ +public class SysUserRole +{ + /** 用户ID */ + private Long userId; + + /** 角色ID */ + private Long roleId; + + public Long getUserId() + { + return userId; + } + + public void setUserId(Long userId) + { + this.userId = userId; + } + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("userId", getUserId()) + .append("roleId", getRoleId()) + .toString(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/MetaVo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/MetaVo.java new file mode 100644 index 0000000..a5d5fdc --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/MetaVo.java @@ -0,0 +1,106 @@ +package com.ruoyi.system.domain.vo; + +import com.ruoyi.common.utils.StringUtils; + +/** + * 路由显示信息 + * + * @author ruoyi + */ +public class MetaVo +{ + /** + * 设置该路由在侧边栏和面包屑中展示的名字 + */ + private String title; + + /** + * 设置该路由的图标,对应路径src/assets/icons/svg + */ + private String icon; + + /** + * 设置为true,则不会被 缓存 + */ + private boolean noCache; + + /** + * 内链地址(http(s)://开头) + */ + private String link; + + public MetaVo() + { + } + + public MetaVo(String title, String icon) + { + this.title = title; + this.icon = icon; + } + + public MetaVo(String title, String icon, boolean noCache) + { + this.title = title; + this.icon = icon; + this.noCache = noCache; + } + + public MetaVo(String title, String icon, String link) + { + this.title = title; + this.icon = icon; + this.link = link; + } + + public MetaVo(String title, String icon, boolean noCache, String link) + { + this.title = title; + this.icon = icon; + this.noCache = noCache; + if (StringUtils.ishttp(link)) + { + this.link = link; + } + } + + public boolean isNoCache() + { + return noCache; + } + + public void setNoCache(boolean noCache) + { + this.noCache = noCache; + } + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public String getIcon() + { + return icon; + } + + public void setIcon(String icon) + { + this.icon = icon; + } + + public String getLink() + { + return link; + } + + public void setLink(String link) + { + this.link = link; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/RouterVo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/RouterVo.java new file mode 100644 index 0000000..afff8c9 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/RouterVo.java @@ -0,0 +1,148 @@ +package com.ruoyi.system.domain.vo; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.List; + +/** + * 路由配置信息 + * + * @author ruoyi + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class RouterVo +{ + /** + * 路由名字 + */ + private String name; + + /** + * 路由地址 + */ + private String path; + + /** + * 是否隐藏路由,当设置 true 的时候该路由不会再侧边栏出现 + */ + private boolean hidden; + + /** + * 重定向地址,当设置 noRedirect 的时候该路由在面包屑导航中不可被点击 + */ + private String redirect; + + /** + * 组件地址 + */ + private String component; + + /** + * 路由参数:如 {"id": 1, "name": "ry"} + */ + private String query; + + /** + * 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面 + */ + private Boolean alwaysShow; + + /** + * 其他元素 + */ + private MetaVo meta; + + /** + * 子路由 + */ + private List children; + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getPath() + { + return path; + } + + public void setPath(String path) + { + this.path = path; + } + + public boolean getHidden() + { + return hidden; + } + + public void setHidden(boolean hidden) + { + this.hidden = hidden; + } + + public String getRedirect() + { + return redirect; + } + + public void setRedirect(String redirect) + { + this.redirect = redirect; + } + + public String getComponent() + { + return component; + } + + public void setComponent(String component) + { + this.component = component; + } + + public String getQuery() + { + return query; + } + + public void setQuery(String query) + { + this.query = query; + } + + public Boolean getAlwaysShow() + { + return alwaysShow; + } + + public void setAlwaysShow(Boolean alwaysShow) + { + this.alwaysShow = alwaysShow; + } + + public MetaVo getMeta() + { + return meta; + } + + public void setMeta(MetaVo meta) + { + this.meta = meta; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysConfigMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysConfigMapper.java new file mode 100644 index 0000000..13d49d6 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysConfigMapper.java @@ -0,0 +1,76 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysConfig; + +/** + * 参数配置 数据层 + * + * @author ruoyi + */ +public interface SysConfigMapper +{ + /** + * 查询参数配置信息 + * + * @param config 参数配置信息 + * @return 参数配置信息 + */ + public SysConfig selectConfig(SysConfig config); + + /** + * 通过ID查询配置 + * + * @param configId 参数ID + * @return 参数配置信息 + */ + public SysConfig selectConfigById(Long configId); + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + public List selectConfigList(SysConfig config); + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数键名 + * @return 参数配置信息 + */ + public SysConfig checkConfigKeyUnique(String configKey); + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int insertConfig(SysConfig config); + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int updateConfig(SysConfig config); + + /** + * 删除参数配置 + * + * @param configId 参数ID + * @return 结果 + */ + public int deleteConfigById(Long configId); + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + * @return 结果 + */ + public int deleteConfigByIds(Long[] configIds); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDeptMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDeptMapper.java new file mode 100644 index 0000000..384a9b6 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDeptMapper.java @@ -0,0 +1,118 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.common.core.domain.entity.SysDept; + +/** + * 部门管理 数据层 + * + * @author ruoyi + */ +public interface SysDeptMapper +{ + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + public List selectDeptList(SysDept dept); + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @param deptCheckStrictly 部门树选择项是否关联显示 + * @return 选中部门列表 + */ + public List selectDeptListByRoleId(@Param("roleId") Long roleId, @Param("deptCheckStrictly") boolean deptCheckStrictly); + + /** + * 根据部门ID查询信息 + * + * @param deptId 部门ID + * @return 部门信息 + */ + public SysDept selectDeptById(Long deptId); + + /** + * 根据ID查询所有子部门 + * + * @param deptId 部门ID + * @return 部门列表 + */ + public List selectChildrenDeptById(Long deptId); + + /** + * 根据ID查询所有子部门(正常状态) + * + * @param deptId 部门ID + * @return 子部门数 + */ + public int selectNormalChildrenDeptById(Long deptId); + + /** + * 是否存在子节点 + * + * @param deptId 部门ID + * @return 结果 + */ + public int hasChildByDeptId(Long deptId); + + /** + * 查询部门是否存在用户 + * + * @param deptId 部门ID + * @return 结果 + */ + public int checkDeptExistUser(Long deptId); + + /** + * 校验部门名称是否唯一 + * + * @param deptName 部门名称 + * @param parentId 父部门ID + * @return 结果 + */ + public SysDept checkDeptNameUnique(@Param("deptName") String deptName, @Param("parentId") Long parentId); + + /** + * 新增部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int insertDept(SysDept dept); + + /** + * 修改部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int updateDept(SysDept dept); + + /** + * 修改所在部门正常状态 + * + * @param deptIds 部门ID组 + */ + public void updateDeptStatusNormal(Long[] deptIds); + + /** + * 修改子元素关系 + * + * @param depts 子元素 + * @return 结果 + */ + public int updateDeptChildren(@Param("depts") List depts); + + /** + * 删除部门管理信息 + * + * @param deptId 部门ID + * @return 结果 + */ + public int deleteDeptById(Long deptId); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java new file mode 100644 index 0000000..a341f1e --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java @@ -0,0 +1,95 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.common.core.domain.entity.SysDictData; + +/** + * 字典表 数据层 + * + * @author ruoyi + */ +public interface SysDictDataMapper +{ + /** + * 根据条件分页查询字典数据 + * + * @param dictData 字典数据信息 + * @return 字典数据集合信息 + */ + public List selectDictDataList(SysDictData dictData); + + /** + * 根据字典类型查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据集合信息 + */ + public List selectDictDataByType(String dictType); + + /** + * 根据字典类型和字典键值查询字典数据信息 + * + * @param dictType 字典类型 + * @param dictValue 字典键值 + * @return 字典标签 + */ + public String selectDictLabel(@Param("dictType") String dictType, @Param("dictValue") String dictValue); + + /** + * 根据字典数据ID查询信息 + * + * @param dictCode 字典数据ID + * @return 字典数据 + */ + public SysDictData selectDictDataById(Long dictCode); + + /** + * 查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据 + */ + public int countDictDataByType(String dictType); + + /** + * 通过字典ID删除字典数据信息 + * + * @param dictCode 字典数据ID + * @return 结果 + */ + public int deleteDictDataById(Long dictCode); + + /** + * 批量删除字典数据信息 + * + * @param dictCodes 需要删除的字典数据ID + * @return 结果 + */ + public int deleteDictDataByIds(Long[] dictCodes); + + /** + * 新增字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int insertDictData(SysDictData dictData); + + /** + * 修改字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int updateDictData(SysDictData dictData); + + /** + * 同步修改字典类型 + * + * @param oldDictType 旧字典类型 + * @param newDictType 新旧字典类型 + * @return 结果 + */ + public int updateDictDataType(@Param("oldDictType") String oldDictType, @Param("newDictType") String newDictType); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictTypeMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictTypeMapper.java new file mode 100644 index 0000000..5fb48fb --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictTypeMapper.java @@ -0,0 +1,83 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.common.core.domain.entity.SysDictType; + +/** + * 字典表 数据层 + * + * @author ruoyi + */ +public interface SysDictTypeMapper +{ + /** + * 根据条件分页查询字典类型 + * + * @param dictType 字典类型信息 + * @return 字典类型集合信息 + */ + public List selectDictTypeList(SysDictType dictType); + + /** + * 根据所有字典类型 + * + * @return 字典类型集合信息 + */ + public List selectDictTypeAll(); + + /** + * 根据字典类型ID查询信息 + * + * @param dictId 字典类型ID + * @return 字典类型 + */ + public SysDictType selectDictTypeById(Long dictId); + + /** + * 根据字典类型查询信息 + * + * @param dictType 字典类型 + * @return 字典类型 + */ + public SysDictType selectDictTypeByType(String dictType); + + /** + * 通过字典ID删除字典信息 + * + * @param dictId 字典ID + * @return 结果 + */ + public int deleteDictTypeById(Long dictId); + + /** + * 批量删除字典类型信息 + * + * @param dictIds 需要删除的字典ID + * @return 结果 + */ + public int deleteDictTypeByIds(Long[] dictIds); + + /** + * 新增字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int insertDictType(SysDictType dictType); + + /** + * 修改字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int updateDictType(SysDictType dictType); + + /** + * 校验字典类型称是否唯一 + * + * @param dictType 字典类型 + * @return 结果 + */ + public SysDictType checkDictTypeUnique(String dictType); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysLogininforMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysLogininforMapper.java new file mode 100644 index 0000000..629866f --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysLogininforMapper.java @@ -0,0 +1,42 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysLogininfor; + +/** + * 系统访问日志情况信息 数据层 + * + * @author ruoyi + */ +public interface SysLogininforMapper +{ + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + public void insertLogininfor(SysLogininfor logininfor); + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + public List selectLogininforList(SysLogininfor logininfor); + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + * @return 结果 + */ + public int deleteLogininforByIds(Long[] infoIds); + + /** + * 清空系统登录日志 + * + * @return 结果 + */ + public int cleanLogininfor(); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysMenuMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysMenuMapper.java new file mode 100644 index 0000000..99c0c50 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysMenuMapper.java @@ -0,0 +1,125 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.common.core.domain.entity.SysMenu; + +/** + * 菜单表 数据层 + * + * @author ruoyi + */ +public interface SysMenuMapper +{ + /** + * 查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + public List selectMenuList(SysMenu menu); + + /** + * 根据用户所有权限 + * + * @return 权限列表 + */ + public List selectMenuPerms(); + + /** + * 根据用户查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + public List selectMenuListByUserId(SysMenu menu); + + /** + * 根据角色ID查询权限 + * + * @param roleId 角色ID + * @return 权限列表 + */ + public List selectMenuPermsByRoleId(Long roleId); + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + public List selectMenuPermsByUserId(Long userId); + + /** + * 根据用户ID查询菜单 + * + * @return 菜单列表 + */ + public List selectMenuTreeAll(); + + /** + * 根据用户ID查询菜单 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuTreeByUserId(Long userId); + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @param menuCheckStrictly 菜单树选择项是否关联显示 + * @return 选中菜单列表 + */ + public List selectMenuListByRoleId(@Param("roleId") Long roleId, @Param("menuCheckStrictly") boolean menuCheckStrictly); + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + public SysMenu selectMenuById(Long menuId); + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int hasChildByMenuId(Long menuId); + + /** + * 新增菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int insertMenu(SysMenu menu); + + /** + * 修改菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int updateMenu(SysMenu menu); + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int deleteMenuById(Long menuId); + + /** + * 校验菜单名称是否唯一 + * + * @param menuName 菜单名称 + * @param parentId 父菜单ID + * @return 结果 + */ + public SysMenu checkMenuNameUnique(@Param("menuName") String menuName, @Param("parentId") Long parentId); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysNoticeMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysNoticeMapper.java new file mode 100644 index 0000000..c34f0a2 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysNoticeMapper.java @@ -0,0 +1,60 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysNotice; + +/** + * 通知公告表 数据层 + * + * @author ruoyi + */ +public interface SysNoticeMapper +{ + /** + * 查询公告信息 + * + * @param noticeId 公告ID + * @return 公告信息 + */ + public SysNotice selectNoticeById(Long noticeId); + + /** + * 查询公告列表 + * + * @param notice 公告信息 + * @return 公告集合 + */ + public List selectNoticeList(SysNotice notice); + + /** + * 新增公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int insertNotice(SysNotice notice); + + /** + * 修改公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int updateNotice(SysNotice notice); + + /** + * 批量删除公告 + * + * @param noticeId 公告ID + * @return 结果 + */ + public int deleteNoticeById(Long noticeId); + + /** + * 批量删除公告信息 + * + * @param noticeIds 需要删除的公告ID + * @return 结果 + */ + public int deleteNoticeByIds(Long[] noticeIds); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOperLogMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOperLogMapper.java new file mode 100644 index 0000000..2ae6457 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOperLogMapper.java @@ -0,0 +1,48 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysOperLog; + +/** + * 操作日志 数据层 + * + * @author ruoyi + */ +public interface SysOperLogMapper +{ + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + */ + public void insertOperlog(SysOperLog operLog); + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + public List selectOperLogList(SysOperLog operLog); + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + public int deleteOperLogByIds(Long[] operIds); + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + public SysOperLog selectOperLogById(Long operId); + + /** + * 清空操作日志 + */ + public void cleanOperLog(); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysPostMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysPostMapper.java new file mode 100644 index 0000000..19be227 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysPostMapper.java @@ -0,0 +1,99 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysPost; + +/** + * 岗位信息 数据层 + * + * @author ruoyi + */ +public interface SysPostMapper +{ + /** + * 查询岗位数据集合 + * + * @param post 岗位信息 + * @return 岗位数据集合 + */ + public List selectPostList(SysPost post); + + /** + * 查询所有岗位 + * + * @return 岗位列表 + */ + public List selectPostAll(); + + /** + * 通过岗位ID查询岗位信息 + * + * @param postId 岗位ID + * @return 角色对象信息 + */ + public SysPost selectPostById(Long postId); + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + public List selectPostListByUserId(Long userId); + + /** + * 查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + public List selectPostsByUserName(String userName); + + /** + * 删除岗位信息 + * + * @param postId 岗位ID + * @return 结果 + */ + public int deletePostById(Long postId); + + /** + * 批量删除岗位信息 + * + * @param postIds 需要删除的岗位ID + * @return 结果 + */ + public int deletePostByIds(Long[] postIds); + + /** + * 修改岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int updatePost(SysPost post); + + /** + * 新增岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int insertPost(SysPost post); + + /** + * 校验岗位名称 + * + * @param postName 岗位名称 + * @return 结果 + */ + public SysPost checkPostNameUnique(String postName); + + /** + * 校验岗位编码 + * + * @param postCode 岗位编码 + * @return 结果 + */ + public SysPost checkPostCodeUnique(String postCode); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleDeptMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleDeptMapper.java new file mode 100644 index 0000000..f9d3a2f --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleDeptMapper.java @@ -0,0 +1,44 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysRoleDept; + +/** + * 角色与部门关联表 数据层 + * + * @author ruoyi + */ +public interface SysRoleDeptMapper +{ + /** + * 通过角色ID删除角色和部门关联 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleDeptByRoleId(Long roleId); + + /** + * 批量删除角色部门关联信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteRoleDept(Long[] ids); + + /** + * 查询部门使用数量 + * + * @param deptId 部门ID + * @return 结果 + */ + public int selectCountRoleDeptByDeptId(Long deptId); + + /** + * 批量新增角色部门信息 + * + * @param roleDeptList 角色部门列表 + * @return 结果 + */ + public int batchRoleDept(List roleDeptList); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMapper.java new file mode 100644 index 0000000..cf2bd8c --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMapper.java @@ -0,0 +1,107 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.common.core.domain.entity.SysRole; + +/** + * 角色表 数据层 + * + * @author ruoyi + */ +public interface SysRoleMapper +{ + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + public List selectRoleList(SysRole role); + + /** + * 根据用户ID查询角色 + * + * @param userId 用户ID + * @return 角色列表 + */ + public List selectRolePermissionByUserId(Long userId); + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + public List selectRoleAll(); + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + public List selectRoleListByUserId(Long userId); + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + public SysRole selectRoleById(Long roleId); + + /** + * 根据用户ID查询角色 + * + * @param userName 用户名 + * @return 角色列表 + */ + public List selectRolesByUserName(String userName); + + /** + * 校验角色名称是否唯一 + * + * @param roleName 角色名称 + * @return 角色信息 + */ + public SysRole checkRoleNameUnique(String roleName); + + /** + * 校验角色权限是否唯一 + * + * @param roleKey 角色权限 + * @return 角色信息 + */ + public SysRole checkRoleKeyUnique(String roleKey); + + /** + * 修改角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRole(SysRole role); + + /** + * 新增角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int insertRole(SysRole role); + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleById(Long roleId); + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + public int deleteRoleByIds(Long[] roleIds); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMenuMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMenuMapper.java new file mode 100644 index 0000000..6602bee --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysRoleMenuMapper.java @@ -0,0 +1,44 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysRoleMenu; + +/** + * 角色与菜单关联表 数据层 + * + * @author ruoyi + */ +public interface SysRoleMenuMapper +{ + /** + * 查询菜单使用数量 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int checkMenuExistRole(Long menuId); + + /** + * 通过角色ID删除角色和菜单关联 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleMenuByRoleId(Long roleId); + + /** + * 批量删除角色菜单关联信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteRoleMenu(Long[] ids); + + /** + * 批量新增角色菜单信息 + * + * @param roleMenuList 角色菜单列表 + * @return 结果 + */ + public int batchRoleMenu(List roleMenuList); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java new file mode 100644 index 0000000..0976eb0 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java @@ -0,0 +1,127 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.common.core.domain.entity.SysUser; + +/** + * 用户表 数据层 + * + * @author ruoyi + */ +public interface SysUserMapper +{ + /** + * 根据条件分页查询用户列表 + * + * @param sysUser 用户信息 + * @return 用户信息集合信息 + */ + public List selectUserList(SysUser sysUser); + + /** + * 根据条件分页查询已配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectAllocatedList(SysUser user); + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUnallocatedList(SysUser user); + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + public SysUser selectUserByUserName(String userName); + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + public SysUser selectUserById(Long userId); + + /** + * 新增用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int insertUser(SysUser user); + + /** + * 修改用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUser(SysUser user); + + /** + * 修改用户头像 + * + * @param userId 用户ID + * @param avatar 头像地址 + * @return 结果 + */ + public int updateUserAvatar(@Param("userId") Long userId, @Param("avatar") String avatar); + + /** + * 重置用户密码 + * + * @param userId 用户ID + * @param password 密码 + * @return 结果 + */ + public int resetUserPwd(@Param("userId") Long userId, @Param("password") String password); + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserById(Long userId); + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + public int deleteUserByIds(Long[] userIds); + + /** + * 校验用户名称是否唯一 + * + * @param userName 用户名称 + * @return 结果 + */ + public SysUser checkUserNameUnique(String userName); + + /** + * 校验手机号码是否唯一 + * + * @param phonenumber 手机号码 + * @return 结果 + */ + public SysUser checkPhoneUnique(String phonenumber); + + /** + * 校验email是否唯一 + * + * @param email 用户邮箱 + * @return 结果 + */ + public SysUser checkEmailUnique(String email); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserPostMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserPostMapper.java new file mode 100644 index 0000000..2a6a720 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserPostMapper.java @@ -0,0 +1,44 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import com.ruoyi.system.domain.SysUserPost; + +/** + * 用户与岗位关联表 数据层 + * + * @author ruoyi + */ +public interface SysUserPostMapper +{ + /** + * 通过用户ID删除用户和岗位关联 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserPostByUserId(Long userId); + + /** + * 通过岗位ID查询岗位使用数量 + * + * @param postId 岗位ID + * @return 结果 + */ + public int countUserPostById(Long postId); + + /** + * 批量删除用户和岗位关联 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteUserPost(Long[] ids); + + /** + * 批量新增用户岗位信息 + * + * @param userPostList 用户岗位列表 + * @return 结果 + */ + public int batchUserPost(List userPostList); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserRoleMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserRoleMapper.java new file mode 100644 index 0000000..3143ec8 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserRoleMapper.java @@ -0,0 +1,62 @@ +package com.ruoyi.system.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.system.domain.SysUserRole; + +/** + * 用户与角色关联表 数据层 + * + * @author ruoyi + */ +public interface SysUserRoleMapper +{ + /** + * 通过用户ID删除用户和角色关联 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserRoleByUserId(Long userId); + + /** + * 批量删除用户和角色关联 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteUserRole(Long[] ids); + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + public int countUserRoleByRoleId(Long roleId); + + /** + * 批量新增用户角色信息 + * + * @param userRoleList 用户角色列表 + * @return 结果 + */ + public int batchUserRole(List userRoleList); + + /** + * 删除用户和角色关联信息 + * + * @param userRole 用户和角色关联信息 + * @return 结果 + */ + public int deleteUserRoleInfo(SysUserRole userRole); + + /** + * 批量取消授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要删除的用户数据ID + * @return 结果 + */ + public int deleteUserRoleInfos(@Param("roleId") Long roleId, @Param("userIds") Long[] userIds); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysConfigService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysConfigService.java new file mode 100644 index 0000000..a31de82 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysConfigService.java @@ -0,0 +1,90 @@ +package com.ruoyi.system.service; + +import com.ruoyi.system.domain.SysConfig; + +import java.util.List; + +/** + * 参数配置 服务层 + * + * @author ruoyi + */ +public interface ISysConfigService +{ + /** + * 查询参数配置信息 + * + * @param configId 参数配置ID + * @return 参数配置信息 + */ + public SysConfig selectConfigById(Long configId); + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数键名 + * @return 参数键值 + */ + public String selectConfigByKey(String configKey); + + /** + * 获取验证码开关 + * + * @return true开启,false关闭 + */ + public boolean selectCaptchaEnabled(); + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + public List selectConfigList(SysConfig config); + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int insertConfig(SysConfig config); + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int updateConfig(SysConfig config); + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + */ + public void deleteConfigByIds(Long[] configIds); + + /** + * 加载参数缓存数据 + */ + public void loadingConfigCache(); + + /** + * 清空参数缓存数据 + */ + public void clearConfigCache(); + + /** + * 重置参数缓存数据 + */ + public void resetConfigCache(); + + /** + * 校验参数键名是否唯一 + * + * @param config 参数信息 + * @return 结果 + */ + public boolean checkConfigKeyUnique(SysConfig config); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDeptService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDeptService.java new file mode 100644 index 0000000..f228208 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDeptService.java @@ -0,0 +1,124 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.common.core.domain.TreeSelect; +import com.ruoyi.common.core.domain.entity.SysDept; + +/** + * 部门管理 服务层 + * + * @author ruoyi + */ +public interface ISysDeptService +{ + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + public List selectDeptList(SysDept dept); + + /** + * 查询部门树结构信息 + * + * @param dept 部门信息 + * @return 部门树信息集合 + */ + public List selectDeptTreeList(SysDept dept); + + /** + * 构建前端所需要树结构 + * + * @param depts 部门列表 + * @return 树结构列表 + */ + public List buildDeptTree(List depts); + + /** + * 构建前端所需要下拉树结构 + * + * @param depts 部门列表 + * @return 下拉树结构列表 + */ + public List buildDeptTreeSelect(List depts); + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @return 选中部门列表 + */ + public List selectDeptListByRoleId(Long roleId); + + /** + * 根据部门ID查询信息 + * + * @param deptId 部门ID + * @return 部门信息 + */ + public SysDept selectDeptById(Long deptId); + + /** + * 根据ID查询所有子部门(正常状态) + * + * @param deptId 部门ID + * @return 子部门数 + */ + public int selectNormalChildrenDeptById(Long deptId); + + /** + * 是否存在部门子节点 + * + * @param deptId 部门ID + * @return 结果 + */ + public boolean hasChildByDeptId(Long deptId); + + /** + * 查询部门是否存在用户 + * + * @param deptId 部门ID + * @return 结果 true 存在 false 不存在 + */ + public boolean checkDeptExistUser(Long deptId); + + /** + * 校验部门名称是否唯一 + * + * @param dept 部门信息 + * @return 结果 + */ + public boolean checkDeptNameUnique(SysDept dept); + + /** + * 校验部门是否有数据权限 + * + * @param deptId 部门id + */ + public void checkDeptDataScope(Long deptId); + + /** + * 新增保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int insertDept(SysDept dept); + + /** + * 修改保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int updateDept(SysDept dept); + + /** + * 删除部门管理信息 + * + * @param deptId 部门ID + * @return 结果 + */ + public int deleteDeptById(Long deptId); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictDataService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictDataService.java new file mode 100644 index 0000000..9bc4f13 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictDataService.java @@ -0,0 +1,60 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.common.core.domain.entity.SysDictData; + +/** + * 字典 业务层 + * + * @author ruoyi + */ +public interface ISysDictDataService +{ + /** + * 根据条件分页查询字典数据 + * + * @param dictData 字典数据信息 + * @return 字典数据集合信息 + */ + public List selectDictDataList(SysDictData dictData); + + /** + * 根据字典类型和字典键值查询字典数据信息 + * + * @param dictType 字典类型 + * @param dictValue 字典键值 + * @return 字典标签 + */ + public String selectDictLabel(String dictType, String dictValue); + + /** + * 根据字典数据ID查询信息 + * + * @param dictCode 字典数据ID + * @return 字典数据 + */ + public SysDictData selectDictDataById(Long dictCode); + + /** + * 批量删除字典数据信息 + * + * @param dictCodes 需要删除的字典数据ID + */ + public void deleteDictDataByIds(Long[] dictCodes); + + /** + * 新增保存字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int insertDictData(SysDictData dictData); + + /** + * 修改保存字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int updateDictData(SysDictData dictData); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictTypeService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictTypeService.java new file mode 100644 index 0000000..01c1c1d --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysDictTypeService.java @@ -0,0 +1,98 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.common.core.domain.entity.SysDictType; + +/** + * 字典 业务层 + * + * @author ruoyi + */ +public interface ISysDictTypeService +{ + /** + * 根据条件分页查询字典类型 + * + * @param dictType 字典类型信息 + * @return 字典类型集合信息 + */ + public List selectDictTypeList(SysDictType dictType); + + /** + * 根据所有字典类型 + * + * @return 字典类型集合信息 + */ + public List selectDictTypeAll(); + + /** + * 根据字典类型查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据集合信息 + */ + public List selectDictDataByType(String dictType); + + /** + * 根据字典类型ID查询信息 + * + * @param dictId 字典类型ID + * @return 字典类型 + */ + public SysDictType selectDictTypeById(Long dictId); + + /** + * 根据字典类型查询信息 + * + * @param dictType 字典类型 + * @return 字典类型 + */ + public SysDictType selectDictTypeByType(String dictType); + + /** + * 批量删除字典信息 + * + * @param dictIds 需要删除的字典ID + */ + public void deleteDictTypeByIds(Long[] dictIds); + + /** + * 加载字典缓存数据 + */ + public void loadingDictCache(); + + /** + * 清空字典缓存数据 + */ + public void clearDictCache(); + + /** + * 重置字典缓存数据 + */ + public void resetDictCache(); + + /** + * 新增保存字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int insertDictType(SysDictType dictType); + + /** + * 修改保存字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int updateDictType(SysDictType dictType); + + /** + * 校验字典类型称是否唯一 + * + * @param dictType 字典类型 + * @return 结果 + */ + public boolean checkDictTypeUnique(SysDictType dictType); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysLogininforService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysLogininforService.java new file mode 100644 index 0000000..ce3151d --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysLogininforService.java @@ -0,0 +1,40 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.system.domain.SysLogininfor; + +/** + * 系统访问日志情况信息 服务层 + * + * @author ruoyi + */ +public interface ISysLogininforService +{ + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + public void insertLogininfor(SysLogininfor logininfor); + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + public List selectLogininforList(SysLogininfor logininfor); + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + * @return 结果 + */ + public int deleteLogininforByIds(Long[] infoIds); + + /** + * 清空系统登录日志 + */ + public void cleanLogininfor(); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysMenuService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysMenuService.java new file mode 100644 index 0000000..7d60696 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysMenuService.java @@ -0,0 +1,144 @@ +package com.ruoyi.system.service; + +import java.util.List; +import java.util.Set; +import com.ruoyi.common.core.domain.TreeSelect; +import com.ruoyi.common.core.domain.entity.SysMenu; +import com.ruoyi.system.domain.vo.RouterVo; + +/** + * 菜单 业务层 + * + * @author ruoyi + */ +public interface ISysMenuService +{ + /** + * 根据用户查询系统菜单列表 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuList(Long userId); + + /** + * 根据用户查询系统菜单列表 + * + * @param menu 菜单信息 + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuList(SysMenu menu, Long userId); + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + public Set selectMenuPermsByUserId(Long userId); + + /** + * 根据角色ID查询权限 + * + * @param roleId 角色ID + * @return 权限列表 + */ + public Set selectMenuPermsByRoleId(Long roleId); + + /** + * 根据用户ID查询菜单树信息 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuTreeByUserId(Long userId); + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @return 选中菜单列表 + */ + public List selectMenuListByRoleId(Long roleId); + + /** + * 构建前端路由所需要的菜单 + * + * @param menus 菜单列表 + * @return 路由列表 + */ + public List buildMenus(List menus); + + /** + * 构建前端所需要树结构 + * + * @param menus 菜单列表 + * @return 树结构列表 + */ + public List buildMenuTree(List menus); + + /** + * 构建前端所需要下拉树结构 + * + * @param menus 菜单列表 + * @return 下拉树结构列表 + */ + public List buildMenuTreeSelect(List menus); + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + public SysMenu selectMenuById(Long menuId); + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 true 存在 false 不存在 + */ + public boolean hasChildByMenuId(Long menuId); + + /** + * 查询菜单是否存在角色 + * + * @param menuId 菜单ID + * @return 结果 true 存在 false 不存在 + */ + public boolean checkMenuExistRole(Long menuId); + + /** + * 新增保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int insertMenu(SysMenu menu); + + /** + * 修改保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int updateMenu(SysMenu menu); + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int deleteMenuById(Long menuId); + + /** + * 校验菜单名称是否唯一 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean checkMenuNameUnique(SysMenu menu); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysNoticeService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysNoticeService.java new file mode 100644 index 0000000..47ce1b7 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysNoticeService.java @@ -0,0 +1,60 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.system.domain.SysNotice; + +/** + * 公告 服务层 + * + * @author ruoyi + */ +public interface ISysNoticeService +{ + /** + * 查询公告信息 + * + * @param noticeId 公告ID + * @return 公告信息 + */ + public SysNotice selectNoticeById(Long noticeId); + + /** + * 查询公告列表 + * + * @param notice 公告信息 + * @return 公告集合 + */ + public List selectNoticeList(SysNotice notice); + + /** + * 新增公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int insertNotice(SysNotice notice); + + /** + * 修改公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int updateNotice(SysNotice notice); + + /** + * 删除公告信息 + * + * @param noticeId 公告ID + * @return 结果 + */ + public int deleteNoticeById(Long noticeId); + + /** + * 批量删除公告信息 + * + * @param noticeIds 需要删除的公告ID + * @return 结果 + */ + public int deleteNoticeByIds(Long[] noticeIds); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOperLogService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOperLogService.java new file mode 100644 index 0000000..4fd8e5a --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOperLogService.java @@ -0,0 +1,48 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.system.domain.SysOperLog; + +/** + * 操作日志 服务层 + * + * @author ruoyi + */ +public interface ISysOperLogService +{ + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + */ + public void insertOperlog(SysOperLog operLog); + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + public List selectOperLogList(SysOperLog operLog); + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + public int deleteOperLogByIds(Long[] operIds); + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + public SysOperLog selectOperLogById(Long operId); + + /** + * 清空操作日志 + */ + public void cleanOperLog(); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysPostService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysPostService.java new file mode 100644 index 0000000..84779bf --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysPostService.java @@ -0,0 +1,99 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.system.domain.SysPost; + +/** + * 岗位信息 服务层 + * + * @author ruoyi + */ +public interface ISysPostService +{ + /** + * 查询岗位信息集合 + * + * @param post 岗位信息 + * @return 岗位列表 + */ + public List selectPostList(SysPost post); + + /** + * 查询所有岗位 + * + * @return 岗位列表 + */ + public List selectPostAll(); + + /** + * 通过岗位ID查询岗位信息 + * + * @param postId 岗位ID + * @return 角色对象信息 + */ + public SysPost selectPostById(Long postId); + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + public List selectPostListByUserId(Long userId); + + /** + * 校验岗位名称 + * + * @param post 岗位信息 + * @return 结果 + */ + public boolean checkPostNameUnique(SysPost post); + + /** + * 校验岗位编码 + * + * @param post 岗位信息 + * @return 结果 + */ + public boolean checkPostCodeUnique(SysPost post); + + /** + * 通过岗位ID查询岗位使用数量 + * + * @param postId 岗位ID + * @return 结果 + */ + public int countUserPostById(Long postId); + + /** + * 删除岗位信息 + * + * @param postId 岗位ID + * @return 结果 + */ + public int deletePostById(Long postId); + + /** + * 批量删除岗位信息 + * + * @param postIds 需要删除的岗位ID + * @return 结果 + */ + public int deletePostByIds(Long[] postIds); + + /** + * 新增保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int insertPost(SysPost post); + + /** + * 修改保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int updatePost(SysPost post); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysRoleService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysRoleService.java new file mode 100644 index 0000000..9185cce --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysRoleService.java @@ -0,0 +1,173 @@ +package com.ruoyi.system.service; + +import java.util.List; +import java.util.Set; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.system.domain.SysUserRole; + +/** + * 角色业务层 + * + * @author ruoyi + */ +public interface ISysRoleService +{ + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + public List selectRoleList(SysRole role); + + /** + * 根据用户ID查询角色列表 + * + * @param userId 用户ID + * @return 角色列表 + */ + public List selectRolesByUserId(Long userId); + + /** + * 根据用户ID查询角色权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + public Set selectRolePermissionByUserId(Long userId); + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + public List selectRoleAll(); + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + public List selectRoleListByUserId(Long userId); + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + public SysRole selectRoleById(Long roleId); + + /** + * 校验角色名称是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + public boolean checkRoleNameUnique(SysRole role); + + /** + * 校验角色权限是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + public boolean checkRoleKeyUnique(SysRole role); + + /** + * 校验角色是否允许操作 + * + * @param role 角色信息 + */ + public void checkRoleAllowed(SysRole role); + + /** + * 校验角色是否有数据权限 + * + * @param roleIds 角色id + */ + public void checkRoleDataScope(Long... roleIds); + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + public int countUserRoleByRoleId(Long roleId); + + /** + * 新增保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int insertRole(SysRole role); + + /** + * 修改保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRole(SysRole role); + + /** + * 修改角色状态 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRoleStatus(SysRole role); + + /** + * 修改数据权限信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int authDataScope(SysRole role); + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleById(Long roleId); + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + public int deleteRoleByIds(Long[] roleIds); + + /** + * 取消授权用户角色 + * + * @param userRole 用户和角色关联信息 + * @return 结果 + */ + public int deleteAuthUser(SysUserRole userRole); + + /** + * 批量取消授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要取消授权的用户数据ID + * @return 结果 + */ + public int deleteAuthUsers(Long roleId, Long[] userIds); + + /** + * 批量选择授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要删除的用户数据ID + * @return 结果 + */ + public int insertAuthUsers(Long roleId, Long[] userIds); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserOnlineService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserOnlineService.java new file mode 100644 index 0000000..8eb5448 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserOnlineService.java @@ -0,0 +1,48 @@ +package com.ruoyi.system.service; + +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.system.domain.SysUserOnline; + +/** + * 在线用户 服务层 + * + * @author ruoyi + */ +public interface ISysUserOnlineService +{ + /** + * 通过登录地址查询信息 + * + * @param ipaddr 登录地址 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByIpaddr(String ipaddr, LoginUser user); + + /** + * 通过用户名称查询信息 + * + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByUserName(String userName, LoginUser user); + + /** + * 通过登录地址/用户名称查询信息 + * + * @param ipaddr 登录地址 + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByInfo(String ipaddr, String userName, LoginUser user); + + /** + * 设置在线用户信息 + * + * @param user 用户信息 + * @return 在线用户 + */ + public SysUserOnline loginUserToUserOnline(LoginUser user); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java new file mode 100644 index 0000000..7a5b679 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java @@ -0,0 +1,206 @@ +package com.ruoyi.system.service; + +import java.util.List; +import com.ruoyi.common.core.domain.entity.SysUser; + +/** + * 用户 业务层 + * + * @author ruoyi + */ +public interface ISysUserService +{ + /** + * 根据条件分页查询用户列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUserList(SysUser user); + + /** + * 根据条件分页查询已分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectAllocatedList(SysUser user); + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUnallocatedList(SysUser user); + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + public SysUser selectUserByUserName(String userName); + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + public SysUser selectUserById(Long userId); + + /** + * 根据用户ID查询用户所属角色组 + * + * @param userName 用户名 + * @return 结果 + */ + public String selectUserRoleGroup(String userName); + + /** + * 根据用户ID查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + public String selectUserPostGroup(String userName); + + /** + * 校验用户名称是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean checkUserNameUnique(SysUser user); + + /** + * 校验手机号码是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean checkPhoneUnique(SysUser user); + + /** + * 校验email是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean checkEmailUnique(SysUser user); + + /** + * 校验用户是否允许操作 + * + * @param user 用户信息 + */ + public void checkUserAllowed(SysUser user); + + /** + * 校验用户是否有数据权限 + * + * @param userId 用户id + */ + public void checkUserDataScope(Long userId); + + /** + * 新增用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int insertUser(SysUser user); + + /** + * 注册用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public boolean registerUser(SysUser user); + + /** + * 修改用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUser(SysUser user); + + /** + * 用户授权角色 + * + * @param userId 用户ID + * @param roleIds 角色组 + */ + public void insertUserAuth(Long userId, Long[] roleIds); + + /** + * 修改用户状态 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUserStatus(SysUser user); + + /** + * 修改用户基本信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUserProfile(SysUser user); + + /** + * 修改用户头像 + * + * @param userId 用户ID + * @param avatar 头像地址 + * @return 结果 + */ + public boolean updateUserAvatar(Long userId, String avatar); + + /** + * 重置用户密码 + * + * @param user 用户信息 + * @return 结果 + */ + public int resetPwd(SysUser user); + + /** + * 重置用户密码 + * + * @param userId 用户ID + * @param password 密码 + * @return 结果 + */ + public int resetUserPwd(Long userId, String password); + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + public int deleteUserById(Long userId); + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + public int deleteUserByIds(Long[] userIds); + + /** + * 导入用户数据 + * + * @param userList 用户数据列表 + * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据 + * @param operName 操作用户 + * @return 结果 + */ + public String importUser(List userList, Boolean isUpdateSupport, String operName); +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java new file mode 100644 index 0000000..95bc8c5 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java @@ -0,0 +1,233 @@ +package com.ruoyi.system.service.impl; + +import com.ruoyi.common.annotation.DataSource; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.enums.DataSourceType; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.SysConfig; +import com.ruoyi.system.mapper.SysConfigMapper; +import com.ruoyi.system.service.ISysConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.util.Collection; +import java.util.List; + +/** + * 参数配置 服务层实现 + * + * @author ruoyi + */ +@Service +public class SysConfigServiceImpl implements ISysConfigService +{ + @Autowired + private SysConfigMapper configMapper; + + @Autowired + private RedisCache redisCache; + + /** + * 项目启动时,初始化参数到缓存 + */ + @PostConstruct + public void init() + { + loadingConfigCache(); + } + + /** + * 查询参数配置信息 + * + * @param configId 参数配置ID + * @return 参数配置信息 + */ + @Override + @DataSource(DataSourceType.MASTER) + public SysConfig selectConfigById(Long configId) + { + SysConfig config = new SysConfig(); + config.setConfigId(configId); + return configMapper.selectConfig(config); + } + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数key + * @return 参数键值 + */ + @Override + public String selectConfigByKey(String configKey) + { + String configValue = Convert.toStr(redisCache.getCacheObject(getCacheKey(configKey))); + if (StringUtils.isNotEmpty(configValue)) + { + return configValue; + } + SysConfig config = new SysConfig(); + config.setConfigKey(configKey); + SysConfig retConfig = configMapper.selectConfig(config); + if (StringUtils.isNotNull(retConfig)) + { + redisCache.setCacheObject(getCacheKey(configKey), retConfig.getConfigValue()); + return retConfig.getConfigValue(); + } + return StringUtils.EMPTY; + } + + /** + * 获取验证码开关 + * + * @return true开启,false关闭 + */ + @Override + public boolean selectCaptchaEnabled() + { + String captchaEnabled = selectConfigByKey("sys.account.captchaEnabled"); + if (StringUtils.isEmpty(captchaEnabled)) + { + return true; + } + return Convert.toBool(captchaEnabled); + } + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + @Override + public List selectConfigList(SysConfig config) + { + return configMapper.selectConfigList(config); + } + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public int insertConfig(SysConfig config) + { + int row = configMapper.insertConfig(config); + if (row > 0) + { + redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + return row; + } + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public int updateConfig(SysConfig config) + { + SysConfig temp = configMapper.selectConfigById(config.getConfigId()); + if (!StringUtils.equals(temp.getConfigKey(), config.getConfigKey())) + { + redisCache.deleteObject(getCacheKey(temp.getConfigKey())); + } + + int row = configMapper.updateConfig(config); + if (row > 0) + { + redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + return row; + } + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + */ + @Override + public void deleteConfigByIds(Long[] configIds) + { + for (Long configId : configIds) + { + SysConfig config = selectConfigById(configId); + if (StringUtils.equals(UserConstants.YES, config.getConfigType())) + { + throw new ServiceException(String.format("内置参数【%1$s】不能删除 ", config.getConfigKey())); + } + configMapper.deleteConfigById(configId); + redisCache.deleteObject(getCacheKey(config.getConfigKey())); + } + } + + /** + * 加载参数缓存数据 + */ + @Override + public void loadingConfigCache() + { + List configsList = configMapper.selectConfigList(new SysConfig()); + for (SysConfig config : configsList) + { + redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + } + + /** + * 清空参数缓存数据 + */ + @Override + public void clearConfigCache() + { + Collection keys = redisCache.keys(CacheConstants.SYS_CONFIG_KEY + "*"); + redisCache.deleteObject(keys); + } + + /** + * 重置参数缓存数据 + */ + @Override + public void resetConfigCache() + { + clearConfigCache(); + loadingConfigCache(); + } + + /** + * 校验参数键名是否唯一 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public boolean checkConfigKeyUnique(SysConfig config) + { + Long configId = StringUtils.isNull(config.getConfigId()) ? -1L : config.getConfigId(); + SysConfig info = configMapper.checkConfigKeyUnique(config.getConfigKey()); + if (StringUtils.isNotNull(info) && info.getConfigId().longValue() != configId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 设置cache key + * + * @param configKey 参数键 + * @return 缓存键key + */ + private String getCacheKey(String configKey) + { + return CacheConstants.SYS_CONFIG_KEY + configKey; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDeptServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDeptServiceImpl.java new file mode 100644 index 0000000..54b605d --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDeptServiceImpl.java @@ -0,0 +1,338 @@ +package com.ruoyi.system.service.impl; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.annotation.DataScope; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.TreeSelect; +import com.ruoyi.common.core.domain.entity.SysDept; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.system.mapper.SysDeptMapper; +import com.ruoyi.system.mapper.SysRoleMapper; +import com.ruoyi.system.service.ISysDeptService; + +/** + * 部门管理 服务实现 + * + * @author ruoyi + */ +@Service +public class SysDeptServiceImpl implements ISysDeptService +{ + @Autowired + private SysDeptMapper deptMapper; + + @Autowired + private SysRoleMapper roleMapper; + + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + @Override + @DataScope(deptAlias = "d") + public List selectDeptList(SysDept dept) + { + return deptMapper.selectDeptList(dept); + } + + /** + * 查询部门树结构信息 + * + * @param dept 部门信息 + * @return 部门树信息集合 + */ + @Override + public List selectDeptTreeList(SysDept dept) + { + List depts = SpringUtils.getAopProxy(this).selectDeptList(dept); + return buildDeptTreeSelect(depts); + } + + /** + * 构建前端所需要树结构 + * + * @param depts 部门列表 + * @return 树结构列表 + */ + @Override + public List buildDeptTree(List depts) + { + List returnList = new ArrayList(); + List tempList = depts.stream().map(SysDept::getDeptId).collect(Collectors.toList()); + for (SysDept dept : depts) + { + // 如果是顶级节点, 遍历该父节点的所有子节点 + if (!tempList.contains(dept.getParentId())) + { + recursionFn(depts, dept); + returnList.add(dept); + } + } + if (returnList.isEmpty()) + { + returnList = depts; + } + return returnList; + } + + /** + * 构建前端所需要下拉树结构 + * + * @param depts 部门列表 + * @return 下拉树结构列表 + */ + @Override + public List buildDeptTreeSelect(List depts) + { + List deptTrees = buildDeptTree(depts); + return deptTrees.stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @return 选中部门列表 + */ + @Override + public List selectDeptListByRoleId(Long roleId) + { + SysRole role = roleMapper.selectRoleById(roleId); + return deptMapper.selectDeptListByRoleId(roleId, role.isDeptCheckStrictly()); + } + + /** + * 根据部门ID查询信息 + * + * @param deptId 部门ID + * @return 部门信息 + */ + @Override + public SysDept selectDeptById(Long deptId) + { + return deptMapper.selectDeptById(deptId); + } + + /** + * 根据ID查询所有子部门(正常状态) + * + * @param deptId 部门ID + * @return 子部门数 + */ + @Override + public int selectNormalChildrenDeptById(Long deptId) + { + return deptMapper.selectNormalChildrenDeptById(deptId); + } + + /** + * 是否存在子节点 + * + * @param deptId 部门ID + * @return 结果 + */ + @Override + public boolean hasChildByDeptId(Long deptId) + { + int result = deptMapper.hasChildByDeptId(deptId); + return result > 0; + } + + /** + * 查询部门是否存在用户 + * + * @param deptId 部门ID + * @return 结果 true 存在 false 不存在 + */ + @Override + public boolean checkDeptExistUser(Long deptId) + { + int result = deptMapper.checkDeptExistUser(deptId); + return result > 0; + } + + /** + * 校验部门名称是否唯一 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public boolean checkDeptNameUnique(SysDept dept) + { + Long deptId = StringUtils.isNull(dept.getDeptId()) ? -1L : dept.getDeptId(); + SysDept info = deptMapper.checkDeptNameUnique(dept.getDeptName(), dept.getParentId()); + if (StringUtils.isNotNull(info) && info.getDeptId().longValue() != deptId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验部门是否有数据权限 + * + * @param deptId 部门id + */ + @Override + public void checkDeptDataScope(Long deptId) + { + if (!SysUser.isAdmin(SecurityUtils.getUserId()) && StringUtils.isNotNull(deptId)) + { + SysDept dept = new SysDept(); + dept.setDeptId(deptId); + List depts = SpringUtils.getAopProxy(this).selectDeptList(dept); + if (StringUtils.isEmpty(depts)) + { + throw new ServiceException("没有权限访问部门数据!"); + } + } + } + + /** + * 新增保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public int insertDept(SysDept dept) + { + SysDept info = deptMapper.selectDeptById(dept.getParentId()); + // 如果父节点不为正常状态,则不允许新增子节点 + if (!UserConstants.DEPT_NORMAL.equals(info.getStatus())) + { + throw new ServiceException("部门停用,不允许新增"); + } + dept.setAncestors(info.getAncestors() + "," + dept.getParentId()); + return deptMapper.insertDept(dept); + } + + /** + * 修改保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public int updateDept(SysDept dept) + { + SysDept newParentDept = deptMapper.selectDeptById(dept.getParentId()); + SysDept oldDept = deptMapper.selectDeptById(dept.getDeptId()); + if (StringUtils.isNotNull(newParentDept) && StringUtils.isNotNull(oldDept)) + { + String newAncestors = newParentDept.getAncestors() + "," + newParentDept.getDeptId(); + String oldAncestors = oldDept.getAncestors(); + dept.setAncestors(newAncestors); + updateDeptChildren(dept.getDeptId(), newAncestors, oldAncestors); + } + int result = deptMapper.updateDept(dept); + if (UserConstants.DEPT_NORMAL.equals(dept.getStatus()) && StringUtils.isNotEmpty(dept.getAncestors()) + && !StringUtils.equals("0", dept.getAncestors())) + { + // 如果该部门是启用状态,则启用该部门的所有上级部门 + updateParentDeptStatusNormal(dept); + } + return result; + } + + /** + * 修改该部门的父级部门状态 + * + * @param dept 当前部门 + */ + private void updateParentDeptStatusNormal(SysDept dept) + { + String ancestors = dept.getAncestors(); + Long[] deptIds = Convert.toLongArray(ancestors); + deptMapper.updateDeptStatusNormal(deptIds); + } + + /** + * 修改子元素关系 + * + * @param deptId 被修改的部门ID + * @param newAncestors 新的父ID集合 + * @param oldAncestors 旧的父ID集合 + */ + public void updateDeptChildren(Long deptId, String newAncestors, String oldAncestors) + { + List children = deptMapper.selectChildrenDeptById(deptId); + for (SysDept child : children) + { + child.setAncestors(child.getAncestors().replaceFirst(oldAncestors, newAncestors)); + } + if (children.size() > 0) + { + deptMapper.updateDeptChildren(children); + } + } + + /** + * 删除部门管理信息 + * + * @param deptId 部门ID + * @return 结果 + */ + @Override + public int deleteDeptById(Long deptId) + { + return deptMapper.deleteDeptById(deptId); + } + + /** + * 递归列表 + */ + private void recursionFn(List list, SysDept t) + { + // 得到子节点列表 + List childList = getChildList(list, t); + t.setChildren(childList); + for (SysDept tChild : childList) + { + if (hasChild(list, tChild)) + { + recursionFn(list, tChild); + } + } + } + + /** + * 得到子节点列表 + */ + private List getChildList(List list, SysDept t) + { + List tlist = new ArrayList(); + Iterator it = list.iterator(); + while (it.hasNext()) + { + SysDept n = (SysDept) it.next(); + if (StringUtils.isNotNull(n.getParentId()) && n.getParentId().longValue() == t.getDeptId().longValue()) + { + tlist.add(n); + } + } + return tlist; + } + + /** + * 判断是否有子节点 + */ + private boolean hasChild(List list, SysDept t) + { + return getChildList(list, t).size() > 0; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictDataServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictDataServiceImpl.java new file mode 100644 index 0000000..fced569 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictDataServiceImpl.java @@ -0,0 +1,111 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.common.utils.DictUtils; +import com.ruoyi.system.mapper.SysDictDataMapper; +import com.ruoyi.system.service.ISysDictDataService; + +/** + * 字典 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysDictDataServiceImpl implements ISysDictDataService +{ + @Autowired + private SysDictDataMapper dictDataMapper; + + /** + * 根据条件分页查询字典数据 + * + * @param dictData 字典数据信息 + * @return 字典数据集合信息 + */ + @Override + public List selectDictDataList(SysDictData dictData) + { + return dictDataMapper.selectDictDataList(dictData); + } + + /** + * 根据字典类型和字典键值查询字典数据信息 + * + * @param dictType 字典类型 + * @param dictValue 字典键值 + * @return 字典标签 + */ + @Override + public String selectDictLabel(String dictType, String dictValue) + { + return dictDataMapper.selectDictLabel(dictType, dictValue); + } + + /** + * 根据字典数据ID查询信息 + * + * @param dictCode 字典数据ID + * @return 字典数据 + */ + @Override + public SysDictData selectDictDataById(Long dictCode) + { + return dictDataMapper.selectDictDataById(dictCode); + } + + /** + * 批量删除字典数据信息 + * + * @param dictCodes 需要删除的字典数据ID + */ + @Override + public void deleteDictDataByIds(Long[] dictCodes) + { + for (Long dictCode : dictCodes) + { + SysDictData data = selectDictDataById(dictCode); + dictDataMapper.deleteDictDataById(dictCode); + List dictDatas = dictDataMapper.selectDictDataByType(data.getDictType()); + DictUtils.setDictCache(data.getDictType(), dictDatas); + } + } + + /** + * 新增保存字典数据信息 + * + * @param data 字典数据信息 + * @return 结果 + */ + @Override + public int insertDictData(SysDictData data) + { + int row = dictDataMapper.insertDictData(data); + if (row > 0) + { + List dictDatas = dictDataMapper.selectDictDataByType(data.getDictType()); + DictUtils.setDictCache(data.getDictType(), dictDatas); + } + return row; + } + + /** + * 修改保存字典数据信息 + * + * @param data 字典数据信息 + * @return 结果 + */ + @Override + public int updateDictData(SysDictData data) + { + int row = dictDataMapper.updateDictData(data); + if (row > 0) + { + List dictDatas = dictDataMapper.selectDictDataByType(data.getDictType()); + DictUtils.setDictCache(data.getDictType(), dictDatas); + } + return row; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java new file mode 100644 index 0000000..7fd9654 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java @@ -0,0 +1,223 @@ +package com.ruoyi.system.service.impl; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysDictData; +import com.ruoyi.common.core.domain.entity.SysDictType; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.DictUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.mapper.SysDictDataMapper; +import com.ruoyi.system.mapper.SysDictTypeMapper; +import com.ruoyi.system.service.ISysDictTypeService; + +/** + * 字典 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysDictTypeServiceImpl implements ISysDictTypeService +{ + @Autowired + private SysDictTypeMapper dictTypeMapper; + + @Autowired + private SysDictDataMapper dictDataMapper; + + /** + * 项目启动时,初始化字典到缓存 + */ + @PostConstruct + public void init() + { + loadingDictCache(); + } + + /** + * 根据条件分页查询字典类型 + * + * @param dictType 字典类型信息 + * @return 字典类型集合信息 + */ + @Override + public List selectDictTypeList(SysDictType dictType) + { + return dictTypeMapper.selectDictTypeList(dictType); + } + + /** + * 根据所有字典类型 + * + * @return 字典类型集合信息 + */ + @Override + public List selectDictTypeAll() + { + return dictTypeMapper.selectDictTypeAll(); + } + + /** + * 根据字典类型查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据集合信息 + */ + @Override + public List selectDictDataByType(String dictType) + { + List dictDatas = DictUtils.getDictCache(dictType); + if (StringUtils.isNotEmpty(dictDatas)) + { + return dictDatas; + } + dictDatas = dictDataMapper.selectDictDataByType(dictType); + if (StringUtils.isNotEmpty(dictDatas)) + { + DictUtils.setDictCache(dictType, dictDatas); + return dictDatas; + } + return null; + } + + /** + * 根据字典类型ID查询信息 + * + * @param dictId 字典类型ID + * @return 字典类型 + */ + @Override + public SysDictType selectDictTypeById(Long dictId) + { + return dictTypeMapper.selectDictTypeById(dictId); + } + + /** + * 根据字典类型查询信息 + * + * @param dictType 字典类型 + * @return 字典类型 + */ + @Override + public SysDictType selectDictTypeByType(String dictType) + { + return dictTypeMapper.selectDictTypeByType(dictType); + } + + /** + * 批量删除字典类型信息 + * + * @param dictIds 需要删除的字典ID + */ + @Override + public void deleteDictTypeByIds(Long[] dictIds) + { + for (Long dictId : dictIds) + { + SysDictType dictType = selectDictTypeById(dictId); + if (dictDataMapper.countDictDataByType(dictType.getDictType()) > 0) + { + throw new ServiceException(String.format("%1$s已分配,不能删除", dictType.getDictName())); + } + dictTypeMapper.deleteDictTypeById(dictId); + DictUtils.removeDictCache(dictType.getDictType()); + } + } + + /** + * 加载字典缓存数据 + */ + @Override + public void loadingDictCache() + { + SysDictData dictData = new SysDictData(); + dictData.setStatus("0"); + Map> dictDataMap = dictDataMapper.selectDictDataList(dictData).stream().collect(Collectors.groupingBy(SysDictData::getDictType)); + for (Map.Entry> entry : dictDataMap.entrySet()) + { + DictUtils.setDictCache(entry.getKey(), entry.getValue().stream().sorted(Comparator.comparing(SysDictData::getDictSort)).collect(Collectors.toList())); + } + } + + /** + * 清空字典缓存数据 + */ + @Override + public void clearDictCache() + { + DictUtils.clearDictCache(); + } + + /** + * 重置字典缓存数据 + */ + @Override + public void resetDictCache() + { + clearDictCache(); + loadingDictCache(); + } + + /** + * 新增保存字典类型信息 + * + * @param dict 字典类型信息 + * @return 结果 + */ + @Override + public int insertDictType(SysDictType dict) + { + int row = dictTypeMapper.insertDictType(dict); + if (row > 0) + { + DictUtils.setDictCache(dict.getDictType(), null); + } + return row; + } + + /** + * 修改保存字典类型信息 + * + * @param dict 字典类型信息 + * @return 结果 + */ + @Override + @Transactional + public int updateDictType(SysDictType dict) + { + SysDictType oldDict = dictTypeMapper.selectDictTypeById(dict.getDictId()); + dictDataMapper.updateDictDataType(oldDict.getDictType(), dict.getDictType()); + int row = dictTypeMapper.updateDictType(dict); + if (row > 0) + { + List dictDatas = dictDataMapper.selectDictDataByType(dict.getDictType()); + DictUtils.setDictCache(dict.getDictType(), dictDatas); + } + return row; + } + + /** + * 校验字典类型称是否唯一 + * + * @param dict 字典类型 + * @return 结果 + */ + @Override + public boolean checkDictTypeUnique(SysDictType dict) + { + Long dictId = StringUtils.isNull(dict.getDictId()) ? -1L : dict.getDictId(); + SysDictType dictType = dictTypeMapper.checkDictTypeUnique(dict.getDictType()); + if (StringUtils.isNotNull(dictType) && dictType.getDictId().longValue() != dictId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysLogininforServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysLogininforServiceImpl.java new file mode 100644 index 0000000..216aecb --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysLogininforServiceImpl.java @@ -0,0 +1,65 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.system.domain.SysLogininfor; +import com.ruoyi.system.mapper.SysLogininforMapper; +import com.ruoyi.system.service.ISysLogininforService; + +/** + * 系统访问日志情况信息 服务层处理 + * + * @author ruoyi + */ +@Service +public class SysLogininforServiceImpl implements ISysLogininforService +{ + + @Autowired + private SysLogininforMapper logininforMapper; + + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + @Override + public void insertLogininfor(SysLogininfor logininfor) + { + logininforMapper.insertLogininfor(logininfor); + } + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + @Override + public List selectLogininforList(SysLogininfor logininfor) + { + return logininforMapper.selectLogininforList(logininfor); + } + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + * @return 结果 + */ + @Override + public int deleteLogininforByIds(Long[] infoIds) + { + return logininforMapper.deleteLogininforByIds(infoIds); + } + + /** + * 清空系统登录日志 + */ + @Override + public void cleanLogininfor() + { + logininforMapper.cleanLogininfor(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java new file mode 100644 index 0000000..b11c281 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java @@ -0,0 +1,543 @@ +package com.ruoyi.system.service.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.TreeSelect; +import com.ruoyi.common.core.domain.entity.SysMenu; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.vo.MetaVo; +import com.ruoyi.system.domain.vo.RouterVo; +import com.ruoyi.system.mapper.SysMenuMapper; +import com.ruoyi.system.mapper.SysRoleMapper; +import com.ruoyi.system.mapper.SysRoleMenuMapper; +import com.ruoyi.system.service.ISysMenuService; + +/** + * 菜单 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysMenuServiceImpl implements ISysMenuService +{ + public static final String PREMISSION_STRING = "perms[\"{0}\"]"; + + @Autowired + private SysMenuMapper menuMapper; + + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysRoleMenuMapper roleMenuMapper; + + /** + * 根据用户查询系统菜单列表 + * + * @param userId 用户ID + * @return 菜单列表 + */ + @Override + public List selectMenuList(Long userId) + { + return selectMenuList(new SysMenu(), userId); + } + + /** + * 查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + @Override + public List selectMenuList(SysMenu menu, Long userId) + { + List menuList = null; + // 管理员显示所有菜单信息 + if (SysUser.isAdmin(userId)) + { + menuList = menuMapper.selectMenuList(menu); + } + else + { + menu.getParams().put("userId", userId); + menuList = menuMapper.selectMenuListByUserId(menu); + } + return menuList; + } + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + @Override + public Set selectMenuPermsByUserId(Long userId) + { + List perms = menuMapper.selectMenuPermsByUserId(userId); + Set permsSet = new HashSet<>(); + for (String perm : perms) + { + if (StringUtils.isNotEmpty(perm)) + { + permsSet.addAll(Arrays.asList(perm.trim().split(","))); + } + } + return permsSet; + } + + /** + * 根据角色ID查询权限 + * + * @param roleId 角色ID + * @return 权限列表 + */ + @Override + public Set selectMenuPermsByRoleId(Long roleId) + { + List perms = menuMapper.selectMenuPermsByRoleId(roleId); + Set permsSet = new HashSet<>(); + for (String perm : perms) + { + if (StringUtils.isNotEmpty(perm)) + { + permsSet.addAll(Arrays.asList(perm.trim().split(","))); + } + } + return permsSet; + } + + /** + * 根据用户ID查询菜单 + * + * @param userId 用户名称 + * @return 菜单列表 + */ + @Override + public List selectMenuTreeByUserId(Long userId) + { + List menus = null; + if (SecurityUtils.isAdmin(userId)) + { + menus = menuMapper.selectMenuTreeAll(); + } + else + { + menus = menuMapper.selectMenuTreeByUserId(userId); + } + return getChildPerms(menus, 0); + } + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @return 选中菜单列表 + */ + @Override + public List selectMenuListByRoleId(Long roleId) + { + SysRole role = roleMapper.selectRoleById(roleId); + return menuMapper.selectMenuListByRoleId(roleId, role.isMenuCheckStrictly()); + } + + /** + * 构建前端路由所需要的菜单 + * + * @param menus 菜单列表 + * @return 路由列表 + */ + @Override + public List buildMenus(List menus) + { + List routers = new LinkedList(); + for (SysMenu menu : menus) + { + RouterVo router = new RouterVo(); + router.setHidden("1".equals(menu.getVisible())); + router.setName(getRouteName(menu)); + router.setPath(getRouterPath(menu)); + router.setComponent(getComponent(menu)); + router.setQuery(menu.getQuery()); + router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath())); + List cMenus = menu.getChildren(); + if (StringUtils.isNotEmpty(cMenus) && UserConstants.TYPE_DIR.equals(menu.getMenuType())) + { + router.setAlwaysShow(true); + router.setRedirect("noRedirect"); + router.setChildren(buildMenus(cMenus)); + } + else if (isMenuFrame(menu)) + { + router.setMeta(null); + List childrenList = new ArrayList(); + RouterVo children = new RouterVo(); + children.setPath(menu.getPath()); + children.setComponent(menu.getComponent()); + children.setName(getRouteName(menu.getRouteName(), menu.getPath())); + children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath())); + children.setQuery(menu.getQuery()); + childrenList.add(children); + router.setChildren(childrenList); + } + else if (menu.getParentId().intValue() == 0 && isInnerLink(menu)) + { + router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon())); + router.setPath("/"); + List childrenList = new ArrayList(); + RouterVo children = new RouterVo(); + String routerPath = innerLinkReplaceEach(menu.getPath()); + children.setPath(routerPath); + children.setComponent(UserConstants.INNER_LINK); + children.setName(getRouteName(menu.getRouteName(), routerPath)); + children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), menu.getPath())); + childrenList.add(children); + router.setChildren(childrenList); + } + routers.add(router); + } + return routers; + } + + /** + * 构建前端所需要树结构 + * + * @param menus 菜单列表 + * @return 树结构列表 + */ + @Override + public List buildMenuTree(List menus) + { + List returnList = new ArrayList(); + List tempList = menus.stream().map(SysMenu::getMenuId).collect(Collectors.toList()); + for (Iterator iterator = menus.iterator(); iterator.hasNext();) + { + SysMenu menu = (SysMenu) iterator.next(); + // 如果是顶级节点, 遍历该父节点的所有子节点 + if (!tempList.contains(menu.getParentId())) + { + recursionFn(menus, menu); + returnList.add(menu); + } + } + if (returnList.isEmpty()) + { + returnList = menus; + } + return returnList; + } + + /** + * 构建前端所需要下拉树结构 + * + * @param menus 菜单列表 + * @return 下拉树结构列表 + */ + @Override + public List buildMenuTreeSelect(List menus) + { + List menuTrees = buildMenuTree(menus); + return menuTrees.stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + @Override + public SysMenu selectMenuById(Long menuId) + { + return menuMapper.selectMenuById(menuId); + } + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public boolean hasChildByMenuId(Long menuId) + { + int result = menuMapper.hasChildByMenuId(menuId); + return result > 0; + } + + /** + * 查询菜单使用数量 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public boolean checkMenuExistRole(Long menuId) + { + int result = roleMenuMapper.checkMenuExistRole(menuId); + return result > 0; + } + + /** + * 新增保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public int insertMenu(SysMenu menu) + { + return menuMapper.insertMenu(menu); + } + + /** + * 修改保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public int updateMenu(SysMenu menu) + { + return menuMapper.updateMenu(menu); + } + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public int deleteMenuById(Long menuId) + { + return menuMapper.deleteMenuById(menuId); + } + + /** + * 校验菜单名称是否唯一 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public boolean checkMenuNameUnique(SysMenu menu) + { + Long menuId = StringUtils.isNull(menu.getMenuId()) ? -1L : menu.getMenuId(); + SysMenu info = menuMapper.checkMenuNameUnique(menu.getMenuName(), menu.getParentId()); + if (StringUtils.isNotNull(info) && info.getMenuId().longValue() != menuId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 获取路由名称 + * + * @param menu 菜单信息 + * @return 路由名称 + */ + public String getRouteName(SysMenu menu) + { + // 非外链并且是一级目录(类型为目录) + if (isMenuFrame(menu)) + { + return StringUtils.EMPTY; + } + return getRouteName(menu.getRouteName(), menu.getPath()); + } + + /** + * 获取路由名称,如没有配置路由名称则取路由地址 + * + * @param name 路由名称 + * @param path 路由地址 + * @return 路由名称(驼峰格式) + */ + public String getRouteName(String name, String path) + { + String routerName = StringUtils.isNotEmpty(name) ? name : path; + return StringUtils.capitalize(routerName); + } + + /** + * 获取路由地址 + * + * @param menu 菜单信息 + * @return 路由地址 + */ + public String getRouterPath(SysMenu menu) + { + String routerPath = menu.getPath(); + // 内链打开外网方式 + if (menu.getParentId().intValue() != 0 && isInnerLink(menu)) + { + routerPath = innerLinkReplaceEach(routerPath); + } + // 非外链并且是一级目录(类型为目录) + if (0 == menu.getParentId().intValue() && UserConstants.TYPE_DIR.equals(menu.getMenuType()) + && UserConstants.NO_FRAME.equals(menu.getIsFrame())) + { + routerPath = "/" + menu.getPath(); + } + // 非外链并且是一级目录(类型为菜单) + else if (isMenuFrame(menu)) + { + routerPath = "/"; + } + return routerPath; + } + + /** + * 获取组件信息 + * + * @param menu 菜单信息 + * @return 组件信息 + */ + public String getComponent(SysMenu menu) + { + String component = UserConstants.LAYOUT; + if (StringUtils.isNotEmpty(menu.getComponent()) && !isMenuFrame(menu)) + { + component = menu.getComponent(); + } + else if (StringUtils.isEmpty(menu.getComponent()) && menu.getParentId().intValue() != 0 && isInnerLink(menu)) + { + component = UserConstants.INNER_LINK; + } + else if (StringUtils.isEmpty(menu.getComponent()) && isParentView(menu)) + { + component = UserConstants.PARENT_VIEW; + } + return component; + } + + /** + * 是否为菜单内部跳转 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isMenuFrame(SysMenu menu) + { + return menu.getParentId().intValue() == 0 && UserConstants.TYPE_MENU.equals(menu.getMenuType()) + && menu.getIsFrame().equals(UserConstants.NO_FRAME); + } + + /** + * 是否为内链组件 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isInnerLink(SysMenu menu) + { + return menu.getIsFrame().equals(UserConstants.NO_FRAME) && StringUtils.ishttp(menu.getPath()); + } + + /** + * 是否为parent_view组件 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isParentView(SysMenu menu) + { + return menu.getParentId().intValue() != 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType()); + } + + /** + * 根据父节点的ID获取所有子节点 + * + * @param list 分类表 + * @param parentId 传入的父节点ID + * @return String + */ + public List getChildPerms(List list, int parentId) + { + List returnList = new ArrayList(); + for (Iterator iterator = list.iterator(); iterator.hasNext();) + { + SysMenu t = (SysMenu) iterator.next(); + // 一、根据传入的某个父节点ID,遍历该父节点的所有子节点 + if (t.getParentId() == parentId) + { + recursionFn(list, t); + returnList.add(t); + } + } + return returnList; + } + + /** + * 递归列表 + * + * @param list 分类表 + * @param t 子节点 + */ + private void recursionFn(List list, SysMenu t) + { + // 得到子节点列表 + List childList = getChildList(list, t); + t.setChildren(childList); + for (SysMenu tChild : childList) + { + if (hasChild(list, tChild)) + { + recursionFn(list, tChild); + } + } + } + + /** + * 得到子节点列表 + */ + private List getChildList(List list, SysMenu t) + { + List tlist = new ArrayList(); + Iterator it = list.iterator(); + while (it.hasNext()) + { + SysMenu n = (SysMenu) it.next(); + if (n.getParentId().longValue() == t.getMenuId().longValue()) + { + tlist.add(n); + } + } + return tlist; + } + + /** + * 判断是否有子节点 + */ + private boolean hasChild(List list, SysMenu t) + { + return getChildList(list, t).size() > 0; + } + + /** + * 内链域名特殊字符替换 + * + * @return 替换后的内链域名 + */ + public String innerLinkReplaceEach(String path) + { + return StringUtils.replaceEach(path, new String[] { Constants.HTTP, Constants.HTTPS, Constants.WWW, ".", ":" }, + new String[] { "", "", "", "/", "/" }); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysNoticeServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysNoticeServiceImpl.java new file mode 100644 index 0000000..765438b --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysNoticeServiceImpl.java @@ -0,0 +1,92 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.system.domain.SysNotice; +import com.ruoyi.system.mapper.SysNoticeMapper; +import com.ruoyi.system.service.ISysNoticeService; + +/** + * 公告 服务层实现 + * + * @author ruoyi + */ +@Service +public class SysNoticeServiceImpl implements ISysNoticeService +{ + @Autowired + private SysNoticeMapper noticeMapper; + + /** + * 查询公告信息 + * + * @param noticeId 公告ID + * @return 公告信息 + */ + @Override + public SysNotice selectNoticeById(Long noticeId) + { + return noticeMapper.selectNoticeById(noticeId); + } + + /** + * 查询公告列表 + * + * @param notice 公告信息 + * @return 公告集合 + */ + @Override + public List selectNoticeList(SysNotice notice) + { + return noticeMapper.selectNoticeList(notice); + } + + /** + * 新增公告 + * + * @param notice 公告信息 + * @return 结果 + */ + @Override + public int insertNotice(SysNotice notice) + { + return noticeMapper.insertNotice(notice); + } + + /** + * 修改公告 + * + * @param notice 公告信息 + * @return 结果 + */ + @Override + public int updateNotice(SysNotice notice) + { + return noticeMapper.updateNotice(notice); + } + + /** + * 删除公告对象 + * + * @param noticeId 公告ID + * @return 结果 + */ + @Override + public int deleteNoticeById(Long noticeId) + { + return noticeMapper.deleteNoticeById(noticeId); + } + + /** + * 批量删除公告信息 + * + * @param noticeIds 需要删除的公告ID + * @return 结果 + */ + @Override + public int deleteNoticeByIds(Long[] noticeIds) + { + return noticeMapper.deleteNoticeByIds(noticeIds); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOperLogServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOperLogServiceImpl.java new file mode 100644 index 0000000..5489815 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOperLogServiceImpl.java @@ -0,0 +1,76 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.system.domain.SysOperLog; +import com.ruoyi.system.mapper.SysOperLogMapper; +import com.ruoyi.system.service.ISysOperLogService; + +/** + * 操作日志 服务层处理 + * + * @author ruoyi + */ +@Service +public class SysOperLogServiceImpl implements ISysOperLogService +{ + @Autowired + private SysOperLogMapper operLogMapper; + + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + */ + @Override + public void insertOperlog(SysOperLog operLog) + { + operLogMapper.insertOperlog(operLog); + } + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + @Override + public List selectOperLogList(SysOperLog operLog) + { + return operLogMapper.selectOperLogList(operLog); + } + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + @Override + public int deleteOperLogByIds(Long[] operIds) + { + return operLogMapper.deleteOperLogByIds(operIds); + } + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + @Override + public SysOperLog selectOperLogById(Long operId) + { + return operLogMapper.selectOperLogById(operId); + } + + /** + * 清空操作日志 + */ + @Override + public void cleanOperLog() + { + operLogMapper.cleanOperLog(); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysPostServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysPostServiceImpl.java new file mode 100644 index 0000000..5e5fe06 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysPostServiceImpl.java @@ -0,0 +1,178 @@ +package com.ruoyi.system.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.SysPost; +import com.ruoyi.system.mapper.SysPostMapper; +import com.ruoyi.system.mapper.SysUserPostMapper; +import com.ruoyi.system.service.ISysPostService; + +/** + * 岗位信息 服务层处理 + * + * @author ruoyi + */ +@Service +public class SysPostServiceImpl implements ISysPostService +{ + @Autowired + private SysPostMapper postMapper; + + @Autowired + private SysUserPostMapper userPostMapper; + + /** + * 查询岗位信息集合 + * + * @param post 岗位信息 + * @return 岗位信息集合 + */ + @Override + public List selectPostList(SysPost post) + { + return postMapper.selectPostList(post); + } + + /** + * 查询所有岗位 + * + * @return 岗位列表 + */ + @Override + public List selectPostAll() + { + return postMapper.selectPostAll(); + } + + /** + * 通过岗位ID查询岗位信息 + * + * @param postId 岗位ID + * @return 角色对象信息 + */ + @Override + public SysPost selectPostById(Long postId) + { + return postMapper.selectPostById(postId); + } + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + @Override + public List selectPostListByUserId(Long userId) + { + return postMapper.selectPostListByUserId(userId); + } + + /** + * 校验岗位名称是否唯一 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public boolean checkPostNameUnique(SysPost post) + { + Long postId = StringUtils.isNull(post.getPostId()) ? -1L : post.getPostId(); + SysPost info = postMapper.checkPostNameUnique(post.getPostName()); + if (StringUtils.isNotNull(info) && info.getPostId().longValue() != postId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验岗位编码是否唯一 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public boolean checkPostCodeUnique(SysPost post) + { + Long postId = StringUtils.isNull(post.getPostId()) ? -1L : post.getPostId(); + SysPost info = postMapper.checkPostCodeUnique(post.getPostCode()); + if (StringUtils.isNotNull(info) && info.getPostId().longValue() != postId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 通过岗位ID查询岗位使用数量 + * + * @param postId 岗位ID + * @return 结果 + */ + @Override + public int countUserPostById(Long postId) + { + return userPostMapper.countUserPostById(postId); + } + + /** + * 删除岗位信息 + * + * @param postId 岗位ID + * @return 结果 + */ + @Override + public int deletePostById(Long postId) + { + return postMapper.deletePostById(postId); + } + + /** + * 批量删除岗位信息 + * + * @param postIds 需要删除的岗位ID + * @return 结果 + */ + @Override + public int deletePostByIds(Long[] postIds) + { + for (Long postId : postIds) + { + SysPost post = selectPostById(postId); + if (countUserPostById(postId) > 0) + { + throw new ServiceException(String.format("%1$s已分配,不能删除", post.getPostName())); + } + } + return postMapper.deletePostByIds(postIds); + } + + /** + * 新增保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public int insertPost(SysPost post) + { + return postMapper.insertPost(post); + } + + /** + * 修改保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public int updatePost(SysPost post) + { + return postMapper.updatePost(post); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java new file mode 100644 index 0000000..e432bb1 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java @@ -0,0 +1,427 @@ +package com.ruoyi.system.service.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.common.annotation.DataScope; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.system.domain.SysRoleDept; +import com.ruoyi.system.domain.SysRoleMenu; +import com.ruoyi.system.domain.SysUserRole; +import com.ruoyi.system.mapper.SysRoleDeptMapper; +import com.ruoyi.system.mapper.SysRoleMapper; +import com.ruoyi.system.mapper.SysRoleMenuMapper; +import com.ruoyi.system.mapper.SysUserRoleMapper; +import com.ruoyi.system.service.ISysRoleService; + +/** + * 角色 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysRoleServiceImpl implements ISysRoleService +{ + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysRoleMenuMapper roleMenuMapper; + + @Autowired + private SysUserRoleMapper userRoleMapper; + + @Autowired + private SysRoleDeptMapper roleDeptMapper; + + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + @Override + @DataScope(deptAlias = "d") + public List selectRoleList(SysRole role) + { + return roleMapper.selectRoleList(role); + } + + /** + * 根据用户ID查询角色 + * + * @param userId 用户ID + * @return 角色列表 + */ + @Override + public List selectRolesByUserId(Long userId) + { + List userRoles = roleMapper.selectRolePermissionByUserId(userId); + List roles = selectRoleAll(); + for (SysRole role : roles) + { + for (SysRole userRole : userRoles) + { + if (role.getRoleId().longValue() == userRole.getRoleId().longValue()) + { + role.setFlag(true); + break; + } + } + } + return roles; + } + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + @Override + public Set selectRolePermissionByUserId(Long userId) + { + List perms = roleMapper.selectRolePermissionByUserId(userId); + Set permsSet = new HashSet<>(); + for (SysRole perm : perms) + { + if (StringUtils.isNotNull(perm)) + { + permsSet.addAll(Arrays.asList(perm.getRoleKey().trim().split(","))); + } + } + return permsSet; + } + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + @Override + public List selectRoleAll() + { + return SpringUtils.getAopProxy(this).selectRoleList(new SysRole()); + } + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + @Override + public List selectRoleListByUserId(Long userId) + { + return roleMapper.selectRoleListByUserId(userId); + } + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + @Override + public SysRole selectRoleById(Long roleId) + { + return roleMapper.selectRoleById(roleId); + } + + /** + * 校验角色名称是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public boolean checkRoleNameUnique(SysRole role) + { + Long roleId = StringUtils.isNull(role.getRoleId()) ? -1L : role.getRoleId(); + SysRole info = roleMapper.checkRoleNameUnique(role.getRoleName()); + if (StringUtils.isNotNull(info) && info.getRoleId().longValue() != roleId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验角色权限是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public boolean checkRoleKeyUnique(SysRole role) + { + Long roleId = StringUtils.isNull(role.getRoleId()) ? -1L : role.getRoleId(); + SysRole info = roleMapper.checkRoleKeyUnique(role.getRoleKey()); + if (StringUtils.isNotNull(info) && info.getRoleId().longValue() != roleId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验角色是否允许操作 + * + * @param role 角色信息 + */ + @Override + public void checkRoleAllowed(SysRole role) + { + if (StringUtils.isNotNull(role.getRoleId()) && role.isAdmin()) + { + throw new ServiceException("不允许操作超级管理员角色"); + } + } + + /** + * 校验角色是否有数据权限 + * + * @param roleIds 角色id + */ + @Override + public void checkRoleDataScope(Long... roleIds) + { + if (!SysUser.isAdmin(SecurityUtils.getUserId())) + { + for (Long roleId : roleIds) + { + SysRole role = new SysRole(); + role.setRoleId(roleId); + List roles = SpringUtils.getAopProxy(this).selectRoleList(role); + if (StringUtils.isEmpty(roles)) + { + throw new ServiceException("没有权限访问角色数据!"); + } + } + } + } + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + @Override + public int countUserRoleByRoleId(Long roleId) + { + return userRoleMapper.countUserRoleByRoleId(roleId); + } + + /** + * 新增保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional + public int insertRole(SysRole role) + { + // 新增角色信息 + roleMapper.insertRole(role); + return insertRoleMenu(role); + } + + /** + * 修改保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional + public int updateRole(SysRole role) + { + // 修改角色信息 + roleMapper.updateRole(role); + // 删除角色与菜单关联 + roleMenuMapper.deleteRoleMenuByRoleId(role.getRoleId()); + return insertRoleMenu(role); + } + + /** + * 修改角色状态 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public int updateRoleStatus(SysRole role) + { + return roleMapper.updateRole(role); + } + + /** + * 修改数据权限信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional + public int authDataScope(SysRole role) + { + // 修改角色信息 + roleMapper.updateRole(role); + // 删除角色与部门关联 + roleDeptMapper.deleteRoleDeptByRoleId(role.getRoleId()); + // 新增角色和部门信息(数据权限) + return insertRoleDept(role); + } + + /** + * 新增角色菜单信息 + * + * @param role 角色对象 + */ + public int insertRoleMenu(SysRole role) + { + int rows = 1; + // 新增用户与角色管理 + List list = new ArrayList(); + for (Long menuId : role.getMenuIds()) + { + SysRoleMenu rm = new SysRoleMenu(); + rm.setRoleId(role.getRoleId()); + rm.setMenuId(menuId); + list.add(rm); + } + if (list.size() > 0) + { + rows = roleMenuMapper.batchRoleMenu(list); + } + return rows; + } + + /** + * 新增角色部门信息(数据权限) + * + * @param role 角色对象 + */ + public int insertRoleDept(SysRole role) + { + int rows = 1; + // 新增角色与部门(数据权限)管理 + List list = new ArrayList(); + for (Long deptId : role.getDeptIds()) + { + SysRoleDept rd = new SysRoleDept(); + rd.setRoleId(role.getRoleId()); + rd.setDeptId(deptId); + list.add(rd); + } + if (list.size() > 0) + { + rows = roleDeptMapper.batchRoleDept(list); + } + return rows; + } + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + @Override + @Transactional + public int deleteRoleById(Long roleId) + { + // 删除角色与菜单关联 + roleMenuMapper.deleteRoleMenuByRoleId(roleId); + // 删除角色与部门关联 + roleDeptMapper.deleteRoleDeptByRoleId(roleId); + return roleMapper.deleteRoleById(roleId); + } + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + @Override + @Transactional + public int deleteRoleByIds(Long[] roleIds) + { + for (Long roleId : roleIds) + { + checkRoleAllowed(new SysRole(roleId)); + checkRoleDataScope(roleId); + SysRole role = selectRoleById(roleId); + if (countUserRoleByRoleId(roleId) > 0) + { + throw new ServiceException(String.format("%1$s已分配,不能删除", role.getRoleName())); + } + } + // 删除角色与菜单关联 + roleMenuMapper.deleteRoleMenu(roleIds); + // 删除角色与部门关联 + roleDeptMapper.deleteRoleDept(roleIds); + return roleMapper.deleteRoleByIds(roleIds); + } + + /** + * 取消授权用户角色 + * + * @param userRole 用户和角色关联信息 + * @return 结果 + */ + @Override + public int deleteAuthUser(SysUserRole userRole) + { + return userRoleMapper.deleteUserRoleInfo(userRole); + } + + /** + * 批量取消授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要取消授权的用户数据ID + * @return 结果 + */ + @Override + public int deleteAuthUsers(Long roleId, Long[] userIds) + { + return userRoleMapper.deleteUserRoleInfos(roleId, userIds); + } + + /** + * 批量选择授权用户角色 + * + * @param roleId 角色ID + * @param userIds 需要授权的用户数据ID + * @return 结果 + */ + @Override + public int insertAuthUsers(Long roleId, Long[] userIds) + { + // 新增用户与角色管理 + List list = new ArrayList(); + for (Long userId : userIds) + { + SysUserRole ur = new SysUserRole(); + ur.setUserId(userId); + ur.setRoleId(roleId); + list.add(ur); + } + return userRoleMapper.batchUserRole(list); + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserOnlineServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserOnlineServiceImpl.java new file mode 100644 index 0000000..f80a877 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserOnlineServiceImpl.java @@ -0,0 +1,96 @@ +package com.ruoyi.system.service.impl; + +import org.springframework.stereotype.Service; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.domain.SysUserOnline; +import com.ruoyi.system.service.ISysUserOnlineService; + +/** + * 在线用户 服务层处理 + * + * @author ruoyi + */ +@Service +public class SysUserOnlineServiceImpl implements ISysUserOnlineService +{ + /** + * 通过登录地址查询信息 + * + * @param ipaddr 登录地址 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByIpaddr(String ipaddr, LoginUser user) + { + if (StringUtils.equals(ipaddr, user.getIpaddr())) + { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 通过用户名称查询信息 + * + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByUserName(String userName, LoginUser user) + { + if (StringUtils.equals(userName, user.getUsername())) + { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 通过登录地址/用户名称查询信息 + * + * @param ipaddr 登录地址 + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByInfo(String ipaddr, String userName, LoginUser user) + { + if (StringUtils.equals(ipaddr, user.getIpaddr()) && StringUtils.equals(userName, user.getUsername())) + { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 设置在线用户信息 + * + * @param user 用户信息 + * @return 在线用户 + */ + @Override + public SysUserOnline loginUserToUserOnline(LoginUser user) + { + if (StringUtils.isNull(user) || StringUtils.isNull(user.getUser())) + { + return null; + } + SysUserOnline sysUserOnline = new SysUserOnline(); + sysUserOnline.setTokenId(user.getToken()); + sysUserOnline.setUserName(user.getUsername()); + sysUserOnline.setIpaddr(user.getIpaddr()); + sysUserOnline.setLoginLocation(user.getLoginLocation()); + sysUserOnline.setBrowser(user.getBrowser()); + sysUserOnline.setOs(user.getOs()); + sysUserOnline.setLoginTime(user.getLoginTime()); + if (StringUtils.isNotNull(user.getUser().getDept())) + { + sysUserOnline.setDeptName(user.getUser().getDept().getDeptName()); + } + return sysUserOnline; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java new file mode 100644 index 0000000..9daf5b3 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java @@ -0,0 +1,550 @@ +package com.ruoyi.system.service.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import javax.validation.Validator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import com.ruoyi.common.annotation.DataScope; +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.entity.SysRole; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.bean.BeanValidators; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.system.domain.SysPost; +import com.ruoyi.system.domain.SysUserPost; +import com.ruoyi.system.domain.SysUserRole; +import com.ruoyi.system.mapper.SysPostMapper; +import com.ruoyi.system.mapper.SysRoleMapper; +import com.ruoyi.system.mapper.SysUserMapper; +import com.ruoyi.system.mapper.SysUserPostMapper; +import com.ruoyi.system.mapper.SysUserRoleMapper; +import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.system.service.ISysDeptService; +import com.ruoyi.system.service.ISysUserService; + +/** + * 用户 业务层处理 + * + * @author ruoyi + */ +@Service +public class SysUserServiceImpl implements ISysUserService +{ + private static final Logger log = LoggerFactory.getLogger(SysUserServiceImpl.class); + + @Autowired + private SysUserMapper userMapper; + + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysPostMapper postMapper; + + @Autowired + private SysUserRoleMapper userRoleMapper; + + @Autowired + private SysUserPostMapper userPostMapper; + + @Autowired + private ISysConfigService configService; + + @Autowired + private ISysDeptService deptService; + + @Autowired + protected Validator validator; + + /** + * 根据条件分页查询用户列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public List selectUserList(SysUser user) + { + return userMapper.selectUserList(user); + } + + /** + * 根据条件分页查询已分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public List selectAllocatedList(SysUser user) + { + return userMapper.selectAllocatedList(user); + } + + /** + * 根据条件分页查询未分配用户角色列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + @DataScope(deptAlias = "d", userAlias = "u") + public List selectUnallocatedList(SysUser user) + { + return userMapper.selectUnallocatedList(user); + } + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + @Override + public SysUser selectUserByUserName(String userName) + { + return userMapper.selectUserByUserName(userName); + } + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + @Override + public SysUser selectUserById(Long userId) + { + return userMapper.selectUserById(userId); + } + + /** + * 查询用户所属角色组 + * + * @param userName 用户名 + * @return 结果 + */ + @Override + public String selectUserRoleGroup(String userName) + { + List list = roleMapper.selectRolesByUserName(userName); + if (CollectionUtils.isEmpty(list)) + { + return StringUtils.EMPTY; + } + return list.stream().map(SysRole::getRoleName).collect(Collectors.joining(",")); + } + + /** + * 查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + @Override + public String selectUserPostGroup(String userName) + { + List list = postMapper.selectPostsByUserName(userName); + if (CollectionUtils.isEmpty(list)) + { + return StringUtils.EMPTY; + } + return list.stream().map(SysPost::getPostName).collect(Collectors.joining(",")); + } + + /** + * 校验用户名称是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public boolean checkUserNameUnique(SysUser user) + { + Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId(); + SysUser info = userMapper.checkUserNameUnique(user.getUserName()); + if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验手机号码是否唯一 + * + * @param user 用户信息 + * @return + */ + @Override + public boolean checkPhoneUnique(SysUser user) + { + Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId(); + SysUser info = userMapper.checkPhoneUnique(user.getPhonenumber()); + if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验email是否唯一 + * + * @param user 用户信息 + * @return + */ + @Override + public boolean checkEmailUnique(SysUser user) + { + Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId(); + SysUser info = userMapper.checkEmailUnique(user.getEmail()); + if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) + { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验用户是否允许操作 + * + * @param user 用户信息 + */ + @Override + public void checkUserAllowed(SysUser user) + { + if (StringUtils.isNotNull(user.getUserId()) && user.isAdmin()) + { + throw new ServiceException("不允许操作超级管理员用户"); + } + } + + /** + * 校验用户是否有数据权限 + * + * @param userId 用户id + */ + @Override + public void checkUserDataScope(Long userId) + { + if (!SysUser.isAdmin(SecurityUtils.getUserId())) + { + SysUser user = new SysUser(); + user.setUserId(userId); + List users = SpringUtils.getAopProxy(this).selectUserList(user); + if (StringUtils.isEmpty(users)) + { + throw new ServiceException("没有权限访问用户数据!"); + } + } + } + + /** + * 新增保存用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + @Transactional + public int insertUser(SysUser user) + { + // 新增用户信息 + int rows = userMapper.insertUser(user); + // 新增用户岗位关联 + insertUserPost(user); + // 新增用户与角色管理 + insertUserRole(user); + return rows; + } + + /** + * 注册用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public boolean registerUser(SysUser user) + { + return userMapper.insertUser(user) > 0; + } + + /** + * 修改保存用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + @Transactional + public int updateUser(SysUser user) + { + Long userId = user.getUserId(); + // 删除用户与角色关联 + userRoleMapper.deleteUserRoleByUserId(userId); + // 新增用户与角色管理 + insertUserRole(user); + // 删除用户与岗位关联 + userPostMapper.deleteUserPostByUserId(userId); + // 新增用户与岗位管理 + insertUserPost(user); + return userMapper.updateUser(user); + } + + /** + * 用户授权角色 + * + * @param userId 用户ID + * @param roleIds 角色组 + */ + @Override + @Transactional + public void insertUserAuth(Long userId, Long[] roleIds) + { + userRoleMapper.deleteUserRoleByUserId(userId); + insertUserRole(userId, roleIds); + } + + /** + * 修改用户状态 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public int updateUserStatus(SysUser user) + { + return userMapper.updateUser(user); + } + + /** + * 修改用户基本信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public int updateUserProfile(SysUser user) + { + return userMapper.updateUser(user); + } + + /** + * 修改用户头像 + * + * @param userId 用户ID + * @param avatar 头像地址 + * @return 结果 + */ + @Override + public boolean updateUserAvatar(Long userId, String avatar) + { + return userMapper.updateUserAvatar(userId, avatar) > 0; + } + + /** + * 重置用户密码 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public int resetPwd(SysUser user) + { + return userMapper.updateUser(user); + } + + /** + * 重置用户密码 + * + * @param userId 用户ID + * @param password 密码 + * @return 结果 + */ + @Override + public int resetUserPwd(Long userId, String password) + { + return userMapper.resetUserPwd(userId, password); + } + + /** + * 新增用户角色信息 + * + * @param user 用户对象 + */ + public void insertUserRole(SysUser user) + { + this.insertUserRole(user.getUserId(), user.getRoleIds()); + } + + /** + * 新增用户岗位信息 + * + * @param user 用户对象 + */ + public void insertUserPost(SysUser user) + { + Long[] posts = user.getPostIds(); + if (StringUtils.isNotEmpty(posts)) + { + // 新增用户与岗位管理 + List list = new ArrayList(posts.length); + for (Long postId : posts) + { + SysUserPost up = new SysUserPost(); + up.setUserId(user.getUserId()); + up.setPostId(postId); + list.add(up); + } + userPostMapper.batchUserPost(list); + } + } + + /** + * 新增用户角色信息 + * + * @param userId 用户ID + * @param roleIds 角色组 + */ + public void insertUserRole(Long userId, Long[] roleIds) + { + if (StringUtils.isNotEmpty(roleIds)) + { + // 新增用户与角色管理 + List list = new ArrayList(roleIds.length); + for (Long roleId : roleIds) + { + SysUserRole ur = new SysUserRole(); + ur.setUserId(userId); + ur.setRoleId(roleId); + list.add(ur); + } + userRoleMapper.batchUserRole(list); + } + } + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + @Override + @Transactional + public int deleteUserById(Long userId) + { + // 删除用户与角色关联 + userRoleMapper.deleteUserRoleByUserId(userId); + // 删除用户与岗位表 + userPostMapper.deleteUserPostByUserId(userId); + return userMapper.deleteUserById(userId); + } + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + @Override + @Transactional + public int deleteUserByIds(Long[] userIds) + { + for (Long userId : userIds) + { + checkUserAllowed(new SysUser(userId)); + checkUserDataScope(userId); + } + // 删除用户与角色关联 + userRoleMapper.deleteUserRole(userIds); + // 删除用户与岗位关联 + userPostMapper.deleteUserPost(userIds); + return userMapper.deleteUserByIds(userIds); + } + + /** + * 导入用户数据 + * + * @param userList 用户数据列表 + * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据 + * @param operName 操作用户 + * @return 结果 + */ + @Override + public String importUser(List userList, Boolean isUpdateSupport, String operName) + { + if (StringUtils.isNull(userList) || userList.size() == 0) + { + throw new ServiceException("导入用户数据不能为空!"); + } + int successNum = 0; + int failureNum = 0; + StringBuilder successMsg = new StringBuilder(); + StringBuilder failureMsg = new StringBuilder(); + for (SysUser user : userList) + { + try + { + // 验证是否存在这个用户 + SysUser u = userMapper.selectUserByUserName(user.getUserName()); + if (StringUtils.isNull(u)) + { + BeanValidators.validateWithException(validator, user); + deptService.checkDeptDataScope(user.getDeptId()); + String password = configService.selectConfigByKey("sys.user.initPassword"); + user.setPassword(SecurityUtils.encryptPassword(password)); + user.setCreateBy(operName); + userMapper.insertUser(user); + successNum++; + successMsg.append("
" + successNum + "、账号 " + user.getUserName() + " 导入成功"); + } + else if (isUpdateSupport) + { + BeanValidators.validateWithException(validator, user); + checkUserAllowed(u); + checkUserDataScope(u.getUserId()); + deptService.checkDeptDataScope(user.getDeptId()); + user.setUserId(u.getUserId()); + user.setUpdateBy(operName); + userMapper.updateUser(user); + successNum++; + successMsg.append("
" + successNum + "、账号 " + user.getUserName() + " 更新成功"); + } + else + { + failureNum++; + failureMsg.append("
" + failureNum + "、账号 " + user.getUserName() + " 已存在"); + } + } + catch (Exception e) + { + failureNum++; + String msg = "
" + failureNum + "、账号 " + user.getUserName() + " 导入失败:"; + failureMsg.append(msg + e.getMessage()); + log.error(msg, e); + } + } + if (failureNum > 0) + { + failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:"); + throw new ServiceException(failureMsg.toString()); + } + else + { + successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:"); + } + return successMsg.toString(); + } +} diff --git a/ruoyi-system/src/main/resources/mapper/system/SysConfigMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysConfigMapper.xml new file mode 100644 index 0000000..a5ff114 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysConfigMapper.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + select config_id, config_name, config_key, config_value, config_type, create_by, create_time, update_by, update_time, remark + from sys_config + + + + + + + and config_id = #{configId} + + + and config_key = #{configKey} + + + + + + + + + + + + + + insert into sys_config ( + config_name, + config_key, + config_value, + config_type, + create_by, + remark, + create_time + )values( + #{configName}, + #{configKey}, + #{configValue}, + #{configType}, + #{createBy}, + #{remark}, + sysdate() + ) + + + + update sys_config + + config_name = #{configName}, + config_key = #{configKey}, + config_value = #{configValue}, + config_type = #{configType}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where config_id = #{configId} + + + + delete from sys_config where config_id = #{configId} + + + + delete from sys_config where config_id in + + #{configId} + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml new file mode 100644 index 0000000..cf439f6 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time + from sys_dept d + + + + + + + + + + + + + + + + + + + + insert into sys_dept( + dept_id, + parent_id, + dept_name, + ancestors, + order_num, + leader, + phone, + email, + status, + create_by, + create_time + )values( + #{deptId}, + #{parentId}, + #{deptName}, + #{ancestors}, + #{orderNum}, + #{leader}, + #{phone}, + #{email}, + #{status}, + #{createBy}, + sysdate() + ) + + + + update sys_dept + + parent_id = #{parentId}, + dept_name = #{deptName}, + ancestors = #{ancestors}, + order_num = #{orderNum}, + leader = #{leader}, + phone = #{phone}, + email = #{email}, + status = #{status}, + update_by = #{updateBy}, + update_time = sysdate() + + where dept_id = #{deptId} + + + + update sys_dept set ancestors = + + when #{item.deptId} then #{item.ancestors} + + where dept_id in + + #{item.deptId} + + + + + update sys_dept set status = '0' where dept_id in + + #{deptId} + + + + + update sys_dept set del_flag = '2' where dept_id = #{deptId} + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysDictDataMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysDictDataMapper.xml new file mode 100644 index 0000000..3b94b7f --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysDictDataMapper.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + select dict_code, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark + from sys_dict_data + + + + + + + + + + + + + + delete from sys_dict_data where dict_code = #{dictCode} + + + + delete from sys_dict_data where dict_code in + + #{dictCode} + + + + + update sys_dict_data + + dict_sort = #{dictSort}, + dict_label = #{dictLabel}, + dict_value = #{dictValue}, + dict_type = #{dictType}, + css_class = #{cssClass}, + list_class = #{listClass}, + is_default = #{isDefault}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where dict_code = #{dictCode} + + + + update sys_dict_data set dict_type = #{newDictType} where dict_type = #{oldDictType} + + + + insert into sys_dict_data( + dict_sort, + dict_label, + dict_value, + dict_type, + css_class, + list_class, + is_default, + status, + remark, + create_by, + create_time + )values( + #{dictSort}, + #{dictLabel}, + #{dictValue}, + #{dictType}, + #{cssClass}, + #{listClass}, + #{isDefault}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml new file mode 100644 index 0000000..438d484 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + select dict_id, dict_name, dict_type, status, create_by, create_time, remark + from sys_dict_type + + + + + + + + + + + + + + delete from sys_dict_type where dict_id = #{dictId} + + + + delete from sys_dict_type where dict_id in + + #{dictId} + + + + + update sys_dict_type + + dict_name = #{dictName}, + dict_type = #{dictType}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where dict_id = #{dictId} + + + + insert into sys_dict_type( + dict_name, + dict_type, + status, + remark, + create_by, + create_time + )values( + #{dictName}, + #{dictType}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysLogininforMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysLogininforMapper.xml new file mode 100644 index 0000000..822d665 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysLogininforMapper.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + insert into sys_logininfor (user_name, status, ipaddr, login_location, browser, os, msg, login_time) + values (#{userName}, #{status}, #{ipaddr}, #{loginLocation}, #{browser}, #{os}, #{msg}, sysdate()) + + + + + + delete from sys_logininfor where info_id in + + #{infoId} + + + + + truncate table sys_logininfor + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysMenuMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysMenuMapper.xml new file mode 100644 index 0000000..84e87c9 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysMenuMapper.xml @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select menu_id, menu_name, parent_id, order_num, path, component, `query`, route_name, is_frame, is_cache, menu_type, visible, status, ifnull(perms,'') as perms, icon, create_time + from sys_menu + + + + + + + + + + + + + + + + + + + + + + + + + + update sys_menu + + menu_name = #{menuName}, + parent_id = #{parentId}, + order_num = #{orderNum}, + path = #{path}, + component = #{component}, + `query` = #{query}, + route_name = #{routeName}, + is_frame = #{isFrame}, + is_cache = #{isCache}, + menu_type = #{menuType}, + visible = #{visible}, + status = #{status}, + perms = #{perms}, + icon = #{icon}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where menu_id = #{menuId} + + + + insert into sys_menu( + menu_id, + parent_id, + menu_name, + order_num, + path, + component, + `query`, + route_name, + is_frame, + is_cache, + menu_type, + visible, + status, + perms, + icon, + remark, + create_by, + create_time + )values( + #{menuId}, + #{parentId}, + #{menuName}, + #{orderNum}, + #{path}, + #{component}, + #{query}, + #{routeName}, + #{isFrame}, + #{isCache}, + #{menuType}, + #{visible}, + #{status}, + #{perms}, + #{icon}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + delete from sys_menu where menu_id = #{menuId} + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysNoticeMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysNoticeMapper.xml new file mode 100644 index 0000000..65d3079 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysNoticeMapper.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + select notice_id, notice_title, notice_type, cast(notice_content as char) as notice_content, status, create_by, create_time, update_by, update_time, remark + from sys_notice + + + + + + + + insert into sys_notice ( + notice_title, + notice_type, + notice_content, + status, + remark, + create_by, + create_time + )values( + #{noticeTitle}, + #{noticeType}, + #{noticeContent}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + update sys_notice + + notice_title = #{noticeTitle}, + notice_type = #{noticeType}, + notice_content = #{noticeContent}, + status = #{status}, + update_by = #{updateBy}, + update_time = sysdate() + + where notice_id = #{noticeId} + + + + delete from sys_notice where notice_id = #{noticeId} + + + + delete from sys_notice where notice_id in + + #{noticeId} + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysOperLogMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysOperLogMapper.xml new file mode 100644 index 0000000..201db07 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysOperLogMapper.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + select oper_id, title, business_type, method, request_method, operator_type, oper_name, dept_name, oper_url, oper_ip, oper_location, oper_param, json_result, status, error_msg, oper_time, cost_time + from sys_oper_log + + + + insert into sys_oper_log(title, business_type, method, request_method, operator_type, oper_name, dept_name, oper_url, oper_ip, oper_location, oper_param, json_result, status, error_msg, cost_time, oper_time) + values (#{title}, #{businessType}, #{method}, #{requestMethod}, #{operatorType}, #{operName}, #{deptName}, #{operUrl}, #{operIp}, #{operLocation}, #{operParam}, #{jsonResult}, #{status}, #{errorMsg}, #{costTime}, sysdate()) + + + + + + delete from sys_oper_log where oper_id in + + #{operId} + + + + + + + truncate table sys_oper_log + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysPostMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysPostMapper.xml new file mode 100644 index 0000000..227c459 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysPostMapper.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + select post_id, post_code, post_name, post_sort, status, create_by, create_time, remark + from sys_post + + + + + + + + + + + + + + + + + + update sys_post + + post_code = #{postCode}, + post_name = #{postName}, + post_sort = #{postSort}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where post_id = #{postId} + + + + insert into sys_post( + post_id, + post_code, + post_name, + post_sort, + status, + remark, + create_by, + create_time + )values( + #{postId}, + #{postCode}, + #{postName}, + #{postSort}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + delete from sys_post where post_id = #{postId} + + + + delete from sys_post where post_id in + + #{postId} + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml new file mode 100644 index 0000000..7c4139b --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + delete from sys_role_dept where role_id=#{roleId} + + + + + + delete from sys_role_dept where role_id in + + #{roleId} + + + + + insert into sys_role_dept(role_id, dept_id) values + + (#{item.roleId},#{item.deptId}) + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysRoleMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysRoleMapper.xml new file mode 100644 index 0000000..955d4ee --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysRoleMapper.xml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + select distinct r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.menu_check_strictly, r.dept_check_strictly, + r.status, r.del_flag, r.create_time, r.remark + from sys_role r + left join sys_user_role ur on ur.role_id = r.role_id + left join sys_user u on u.user_id = ur.user_id + left join sys_dept d on u.dept_id = d.dept_id + + + + + + + + + + + + + + + + + + + + insert into sys_role( + role_id, + role_name, + role_key, + role_sort, + data_scope, + menu_check_strictly, + dept_check_strictly, + status, + remark, + create_by, + create_time + )values( + #{roleId}, + #{roleName}, + #{roleKey}, + #{roleSort}, + #{dataScope}, + #{menuCheckStrictly}, + #{deptCheckStrictly}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + + + update sys_role + + role_name = #{roleName}, + role_key = #{roleKey}, + role_sort = #{roleSort}, + data_scope = #{dataScope}, + menu_check_strictly = #{menuCheckStrictly}, + dept_check_strictly = #{deptCheckStrictly}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where role_id = #{roleId} + + + + update sys_role set del_flag = '2' where role_id = #{roleId} + + + + update sys_role set del_flag = '2' where role_id in + + #{roleId} + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml new file mode 100644 index 0000000..cb60a85 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + delete from sys_role_menu where role_id=#{roleId} + + + + delete from sys_role_menu where role_id in + + #{roleId} + + + + + insert into sys_role_menu(role_id, menu_id) values + + (#{item.roleId},#{item.menuId}) + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml new file mode 100644 index 0000000..29c7ad5 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.pwd_update_date, u.create_by, u.create_time, u.remark, + d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status, + r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status + from sys_user u + left join sys_dept d on u.dept_id = d.dept_id + left join sys_user_role ur on u.user_id = ur.user_id + left join sys_role r on r.role_id = ur.role_id + + + + + + + + + + + + + + + + + + + + insert into sys_user( + user_id, + dept_id, + user_name, + nick_name, + email, + avatar, + phonenumber, + sex, + password, + status, + pwd_update_date, + create_by, + remark, + create_time + )values( + #{userId}, + #{deptId}, + #{userName}, + #{nickName}, + #{email}, + #{avatar}, + #{phonenumber}, + #{sex}, + #{password}, + #{status}, + #{pwdUpdateDate}, + #{createBy}, + #{remark}, + sysdate() + ) + + + + update sys_user + + dept_id = #{deptId}, + nick_name = #{nickName}, + email = #{email}, + phonenumber = #{phonenumber}, + sex = #{sex}, + avatar = #{avatar}, + password = #{password}, + status = #{status}, + login_ip = #{loginIp}, + login_date = #{loginDate}, + update_by = #{updateBy}, + remark = #{remark}, + update_time = sysdate() + + where user_id = #{userId} + + + + update sys_user set status = #{status} where user_id = #{userId} + + + + update sys_user set avatar = #{avatar} where user_id = #{userId} + + + + update sys_user set pwd_update_date = sysdate(), password = #{password} where user_id = #{userId} + + + + update sys_user set del_flag = '2' where user_id = #{userId} + + + + update sys_user set del_flag = '2' where user_id in + + #{userId} + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysUserPostMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysUserPostMapper.xml new file mode 100644 index 0000000..2b90bc4 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysUserPostMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + delete from sys_user_post where user_id=#{userId} + + + + + + delete from sys_user_post where user_id in + + #{userId} + + + + + insert into sys_user_post(user_id, post_id) values + + (#{item.userId},#{item.postId}) + + + + \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/SysUserRoleMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysUserRoleMapper.xml new file mode 100644 index 0000000..dd72689 --- /dev/null +++ b/ruoyi-system/src/main/resources/mapper/system/SysUserRoleMapper.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + delete from sys_user_role where user_id=#{userId} + + + + + + delete from sys_user_role where user_id in + + #{userId} + + + + + insert into sys_user_role(user_id, role_id) values + + (#{item.userId},#{item.roleId}) + + + + + delete from sys_user_role where user_id=#{userId} and role_id=#{roleId} + + + + delete from sys_user_role where role_id=#{roleId} and user_id in + + #{userId} + + + \ No newline at end of file