From 2b8d36284aaf9dd36f9c194a3e9dcec4998dccf1 Mon Sep 17 00:00:00 2001 From: hhb <839062268@qq.com> Date: Tue, 31 Oct 2023 23:40:08 +0800 Subject: [PATCH] init --- .gitignore | 8 + LICENSE | 191 +++ README.md | 1 + eladmin-common/pom.xml | 26 + .../zhengjie/annotation/AnonymousAccess.java | 30 + .../zhengjie/annotation/DataPermission.java | 47 + .../java/me/zhengjie/annotation/Limit.java | 49 + .../java/me/zhengjie/annotation/Query.java | 92 ++ .../rest/AnonymousDeleteMapping.java | 91 ++ .../annotation/rest/AnonymousGetMapping.java | 90 ++ .../rest/AnonymousPatchMapping.java | 91 ++ .../annotation/rest/AnonymousPostMapping.java | 91 ++ .../annotation/rest/AnonymousPutMapping.java | 91 ++ .../java/me/zhengjie/aspect/LimitAspect.java | 99 ++ .../java/me/zhengjie/aspect/LimitType.java | 27 + .../main/java/me/zhengjie/base/BaseDTO.java | 40 + .../java/me/zhengjie/base/BaseEntity.java | 85 ++ .../java/me/zhengjie/base/BaseMapper.java | 53 + .../me/zhengjie/config/AuditorConfig.java | 45 + .../me/zhengjie/config/AuthorityConfig.java | 37 + .../me/zhengjie/config/FileProperties.java | 60 + .../java/me/zhengjie/config/RedisConfig.java | 225 +++ .../me/zhengjie/config/RsaProperties.java | 38 + .../me/zhengjie/config/SwaggerConfig.java | 151 ++ .../exception/BadConfigurationException.java | 98 ++ .../exception/BadRequestException.java | 40 + .../exception/EntityExistException.java | 34 + .../exception/EntityNotFoundException.java | 34 + .../zhengjie/exception/handler/ApiError.java | 49 + .../handler/GlobalExceptionHandler.java | 113 ++ .../main/java/me/zhengjie/utils/CacheKey.java | 58 + .../main/java/me/zhengjie/utils/CallBack.java | 43 + .../java/me/zhengjie/utils/CloseUtil.java | 47 + .../main/java/me/zhengjie/utils/DateUtil.java | 160 +++ .../java/me/zhengjie/utils/ElConstant.java | 34 + .../java/me/zhengjie/utils/EncryptUtils.java | 100 ++ .../main/java/me/zhengjie/utils/FileUtil.java | 395 ++++++ .../java/me/zhengjie/utils/PageResult.java | 16 + .../main/java/me/zhengjie/utils/PageUtil.java | 63 + .../java/me/zhengjie/utils/QueryHelp.java | 218 +++ .../java/me/zhengjie/utils/RedisUtils.java | 725 ++++++++++ .../java/me/zhengjie/utils/RequestHolder.java | 33 + .../main/java/me/zhengjie/utils/RsaUtils.java | 198 +++ .../java/me/zhengjie/utils/SecurityUtils.java | 99 ++ .../zhengjie/utils/SpringContextHolder.java | 156 +++ .../java/me/zhengjie/utils/StringUtils.java | 232 ++++ .../java/me/zhengjie/utils/ThrowableUtil.java | 37 + .../me/zhengjie/utils/ValidationUtil.java | 46 + .../me/zhengjie/utils/enums/CodeBiEnum.java | 50 + .../me/zhengjie/utils/enums/CodeEnum.java | 46 + .../zhengjie/utils/enums/DataScopeEnum.java | 53 + .../utils/enums/RequestMethodEnum.java | 74 + .../java/me/zhengjie/utils/DateUtilsTest.java | 26 + .../me/zhengjie/utils/EncryptUtilsTest.java | 33 + .../java/me/zhengjie/utils/FileUtilTest.java | 36 + .../me/zhengjie/utils/StringUtilsTest.java | 48 + eladmin-generator/pom.xml | 39 + .../java/me/zhengjie/domain/ColumnInfo.java | 97 ++ .../java/me/zhengjie/domain/GenConfig.java | 78 ++ .../java/me/zhengjie/domain/vo/TableInfo.java | 48 + .../repository/ColumnInfoRepository.java | 34 + .../repository/GenConfigRepository.java | 33 + .../me/zhengjie/rest/GenConfigController.java | 51 + .../me/zhengjie/rest/GeneratorController.java | 109 ++ .../me/zhengjie/service/GenConfigService.java | 40 + .../me/zhengjie/service/GeneratorService.java | 98 ++ .../service/impl/GenConfigServiceImpl.java | 67 + .../service/impl/GeneratorServiceImpl.java | 204 +++ .../main/java/me/zhengjie/utils/ColUtil.java | 54 + .../main/java/me/zhengjie/utils/GenUtil.java | 422 ++++++ .../src/main/resources/generator.properties | 27 + .../resources/template/admin/Controller.ftl | 89 ++ .../src/main/resources/template/admin/Dto.ftl | 53 + .../main/resources/template/admin/Entity.ftl | 84 ++ .../main/resources/template/admin/Mapper.ftl | 31 + .../template/admin/QueryCriteria.ftl | 80 ++ .../resources/template/admin/Repository.ftl | 39 + .../main/resources/template/admin/Service.ftl | 82 ++ .../resources/template/admin/ServiceImpl.ftl | 157 +++ .../src/main/resources/template/front/api.ftl | 27 + .../main/resources/template/front/index.ftl | 169 +++ eladmin-logging/pom.xml | 22 + .../main/java/me/zhengjie/annotation/Log.java | 31 + .../java/me/zhengjie/aspect/LogAspect.java | 98 ++ .../main/java/me/zhengjie/domain/SysLog.java | 80 ++ .../me/zhengjie/repository/LogRepository.java | 39 + .../me/zhengjie/rest/SysLogController.java | 111 ++ .../me/zhengjie/service/SysLogService.java | 94 ++ .../zhengjie/service/dto/SysLogErrorDto.java | 46 + .../service/dto/SysLogQueryCriteria.java | 42 + .../zhengjie/service/dto/SysLogSmallDto.java | 40 + .../service/impl/SysLogServiceImpl.java | 180 +++ .../service/mapstruct/LogErrorMapper.java | 31 + .../service/mapstruct/LogSmallMapper.java | 31 + eladmin-system/pom.xml | 108 ++ .../src/main/java/me/zhengjie/AppRun.java | 66 + .../me/zhengjie/config/ConfigurerAdapter.java | 88 ++ .../RelaxedQueryCharsConnectorCustomizer.java | 31 + .../me/zhengjie/config/WebSocketConfig.java | 33 + .../config/thread/AsyncTaskProperties.java | 59 + .../config/thread/CustomExecutorConfig.java | 52 + .../me/zhengjie/modules/mnt/domain/App.java | 67 + .../zhengjie/modules/mnt/domain/Database.java | 57 + .../zhengjie/modules/mnt/domain/Deploy.java | 59 + .../modules/mnt/domain/DeployHistory.java | 62 + .../modules/mnt/domain/ServerDeploy.java | 80 ++ .../modules/mnt/repository/AppRepository.java | 27 + .../mnt/repository/DatabaseRepository.java | 27 + .../repository/DeployHistoryRepository.java | 27 + .../mnt/repository/DeployRepository.java | 27 + .../repository/ServerDeployRepository.java | 34 + .../modules/mnt/rest/AppController.java | 89 ++ .../modules/mnt/rest/DatabaseController.java | 124 ++ .../modules/mnt/rest/DeployController.java | 155 +++ .../mnt/rest/DeployHistoryController.java | 69 + .../mnt/rest/ServerDeployController.java | 97 ++ .../modules/mnt/service/AppService.java | 82 ++ .../modules/mnt/service/DatabaseService.java | 89 ++ .../mnt/service/DeployHistoryService.java | 75 + .../modules/mnt/service/DeployService.java | 117 ++ .../mnt/service/ServerDeployService.java | 96 ++ .../modules/mnt/service/dto/AppDto.java | 71 + .../mnt/service/dto/AppQueryCriteria.java | 38 + .../modules/mnt/service/dto/DatabaseDto.java | 55 + .../service/dto/DatabaseQueryCriteria.java | 44 + .../modules/mnt/service/dto/DeployDto.java | 78 ++ .../mnt/service/dto/DeployHistoryDto.java | 58 + .../dto/DeployHistoryQueryCriteria.java | 41 + .../mnt/service/dto/DeployQueryCriteria.java | 39 + .../mnt/service/dto/ServerDeployDto.java | 61 + .../dto/ServerDeployQueryCriteria.java | 38 + .../mnt/service/impl/AppServiceImpl.java | 120 ++ .../mnt/service/impl/DatabaseServiceImpl.java | 114 ++ .../impl/DeployHistoryServiceImpl.java | 93 ++ .../mnt/service/impl/DeployServiceImpl.java | 430 ++++++ .../service/impl/ServerDeployServiceImpl.java | 122 ++ .../mnt/service/mapstruct/AppMapper.java | 31 + .../mnt/service/mapstruct/DatabaseMapper.java | 31 + .../mapstruct/DeployHistoryMapper.java | 31 + .../mnt/service/mapstruct/DeployMapper.java | 31 + .../service/mapstruct/ServerDeployMapper.java | 31 + .../modules/mnt/util/DataTypeEnum.java | 140 ++ .../modules/mnt/util/ExecuteShellUtil.java | 101 ++ .../modules/mnt/util/ScpClientUtil.java | 105 ++ .../zhengjie/modules/mnt/util/SqlUtils.java | 201 +++ .../modules/mnt/websocket/MsgType.java | 31 + .../modules/mnt/websocket/SocketMsg.java | 33 + .../mnt/websocket/WebSocketServer.java | 134 ++ .../modules/quartz/config/JobRunner.java | 51 + .../modules/quartz/config/QuartzConfig.java | 52 + .../modules/quartz/domain/QuartzJob.java | 85 ++ .../modules/quartz/domain/QuartzLog.java | 67 + .../repository/QuartzJobRepository.java | 34 + .../repository/QuartzLogRepository.java | 28 + .../quartz/rest/QuartzJobController.java | 141 ++ .../quartz/service/QuartzJobService.java | 123 ++ .../quartz/service/dto/JobQueryCriteria.java | 38 + .../service/impl/QuartzJobServiceImpl.java | 197 +++ .../modules/quartz/task/TestTask.java | 41 + .../modules/quartz/utils/ExecutionJob.java | 133 ++ .../modules/quartz/utils/QuartzManage.java | 174 +++ .../modules/quartz/utils/QuartzRunnable.java | 58 + .../config/ConfigBeanConfiguration.java | 43 + .../security/config/SpringSecurityConfig.java | 188 +++ .../security/config/bean/LoginCode.java | 61 + .../security/config/bean/LoginCodeEnum.java | 43 + .../security/config/bean/LoginProperties.java | 125 ++ .../config/bean/SecurityProperties.java | 72 + .../rest/AuthorizationController.java | 149 ++ .../security/rest/OnlineController.java | 70 + .../security/JwtAccessDeniedHandler.java | 37 + .../security/JwtAuthenticationEntryPoint.java | 39 + .../security/security/TokenConfigurer.java | 43 + .../security/security/TokenFilter.java | 109 ++ .../security/security/TokenProvider.java | 135 ++ .../security/service/OnlineUserService.java | 149 ++ .../security/service/UserCacheManager.java | 82 ++ .../service/UserDetailsServiceImpl.java | 72 + .../security/service/dto/AuthUserDto.java | 39 + .../security/service/dto/AuthorityDto.java | 34 + .../security/service/dto/JwtUserDto.java | 80 ++ .../security/service/dto/OnlineUserDto.java | 73 + .../zhengjie/modules/system/domain/Dept.java | 86 ++ .../zhengjie/modules/system/domain/Dict.java | 54 + .../modules/system/domain/DictDetail.java | 56 + .../zhengjie/modules/system/domain/Job.java | 73 + .../zhengjie/modules/system/domain/Menu.java | 110 ++ .../modules/system/domain/Purchase.java | 115 ++ .../modules/system/domain/Reimburse.java | 105 ++ .../zhengjie/modules/system/domain/Role.java | 99 ++ .../zhengjie/modules/system/domain/User.java | 129 ++ .../modules/system/domain/UserBank.java | 73 + .../modules/system/domain/vo/MenuMetaVo.java | 35 + .../modules/system/domain/vo/MenuVo.java | 45 + .../modules/system/domain/vo/UserPassVo.java | 31 + .../system/repository/DeptRepository.java | 69 + .../repository/DictDetailRepository.java | 36 + .../system/repository/DictRepository.java | 43 + .../system/repository/JobRepository.java | 42 + .../system/repository/MenuRepository.java | 85 ++ .../system/repository/PurchaseRepository.java | 28 + .../repository/ReimburseRepository.java | 28 + .../system/repository/RoleRepository.java | 80 ++ .../system/repository/UserBankRepository.java | 33 + .../system/repository/UserRepository.java | 139 ++ .../modules/system/rest/DeptController.java | 129 ++ .../modules/system/rest/DictController.java | 103 ++ .../system/rest/DictDetailController.java | 100 ++ .../modules/system/rest/JobController.java | 96 ++ .../modules/system/rest/LimitController.java | 47 + .../modules/system/rest/MenuController.java | 157 +++ .../system/rest/MonitorController.java | 45 + .../system/rest/PurchaseController.java | 90 ++ .../system/rest/ReimburseController.java | 90 ++ .../modules/system/rest/RoleController.java | 155 +++ .../system/rest/UserBankController.java | 90 ++ .../modules/system/rest/UserController.java | 210 +++ .../modules/system/rest/VerifyController.java | 76 ++ .../modules/system/service/DataService.java | 34 + .../modules/system/service/DeptService.java | 124 ++ .../system/service/DictDetailService.java | 63 + .../modules/system/service/DictService.java | 75 + .../modules/system/service/JobService.java | 88 ++ .../modules/system/service/MenuService.java | 126 ++ .../system/service/MonitorService.java | 31 + .../system/service/PurchaseService.java | 83 ++ .../system/service/ReimburseService.java | 83 ++ .../modules/system/service/RoleService.java | 137 ++ .../system/service/UserBankService.java | 83 ++ .../modules/system/service/UserService.java | 133 ++ .../modules/system/service/VerifyService.java | 41 + .../modules/system/service/dto/DeptDto.java | 76 ++ .../system/service/dto/DeptQueryCriteria.java | 46 + .../system/service/dto/DeptSmallDto.java | 31 + .../system/service/dto/DictDetailDto.java | 40 + .../service/dto/DictDetailQueryCriteria.java | 33 + .../modules/system/service/dto/DictDto.java | 39 + .../system/service/dto/DictQueryCriteria.java | 30 + .../system/service/dto/DictSmallDto.java | 31 + .../modules/system/service/dto/JobDto.java | 46 + .../system/service/dto/JobQueryCriteria.java | 40 + .../system/service/dto/JobSmallDto.java | 33 + .../modules/system/service/dto/MenuDto.java | 91 ++ .../system/service/dto/MenuQueryCriteria.java | 41 + .../system/service/dto/PurchaseDto.java | 93 ++ .../service/dto/PurchaseQueryCriteria.java | 33 + .../system/service/dto/ReimburseDto.java | 81 ++ .../service/dto/ReimburseQueryCriteria.java | 33 + .../modules/system/service/dto/RoleDto.java | 63 + .../system/service/dto/RoleQueryCriteria.java | 36 + .../system/service/dto/RoleSmallDto.java | 35 + .../system/service/dto/UserBankDto.java | 61 + .../service/dto/UserBankQueryCriteria.java | 29 + .../modules/system/service/dto/UserDto.java | 68 + .../system/service/dto/UserLoginDto.java | 28 + .../system/service/dto/UserQueryCriteria.java | 49 + .../system/service/impl/DataServiceImpl.java | 91 ++ .../system/service/impl/DeptServiceImpl.java | 283 ++++ .../service/impl/DictDetailServiceImpl.java | 95 ++ .../system/service/impl/DictServiceImpl.java | 122 ++ .../system/service/impl/JobServiceImpl.java | 126 ++ .../system/service/impl/MenuServiceImpl.java | 359 +++++ .../service/impl/MonitorServiceImpl.java | 192 +++ .../service/impl/PurchaseServiceImpl.java | 127 ++ .../service/impl/ReimburseServiceImpl.java | 124 ++ .../system/service/impl/RoleServiceImpl.java | 224 +++ .../service/impl/UserBankServiceImpl.java | 127 ++ .../system/service/impl/UserServiceImpl.java | 277 ++++ .../service/impl/VerifyServiceImpl.java | 81 ++ .../system/service/mapstruct/DeptMapper.java | 30 + .../service/mapstruct/DeptSmallMapper.java | 31 + .../service/mapstruct/DictDetailMapper.java | 31 + .../system/service/mapstruct/DictMapper.java | 31 + .../service/mapstruct/DictSmallMapper.java | 31 + .../system/service/mapstruct/JobMapper.java | 30 + .../service/mapstruct/JobSmallMapper.java | 31 + .../system/service/mapstruct/MenuMapper.java | 30 + .../service/mapstruct/PurchaseMapper.java | 32 + .../service/mapstruct/ReimburseMapper.java | 32 + .../system/service/mapstruct/RoleMapper.java | 31 + .../service/mapstruct/RoleSmallMapper.java | 31 + .../service/mapstruct/UserBankMapper.java | 32 + .../service/mapstruct/UserLoginMapper.java | 30 + .../system/service/mapstruct/UserMapper.java | 30 + eladmin-system/src/main/resources/banner.txt | 8 + .../main/resources/config/application-dev.yml | 116 ++ .../resources/config/application-prod.yml | 125 ++ .../src/main/resources/config/application.yml | 60 + .../main/resources/log4jdbc.log4j2.properties | 4 + eladmin-system/src/main/resources/logback.xml | 45 + .../src/main/resources/template/email.ftl | 48 + .../src/main/resources/template/taskAlarm.ftl | 69 + .../EladminSystemApplicationTests.java | 16 + eladmin-tools/pom.xml | 50 + .../me/zhengjie/config/MultipartConfig.java | 47 + .../java/me/zhengjie/domain/AlipayConfig.java | 76 ++ .../java/me/zhengjie/domain/EmailConfig.java | 58 + .../java/me/zhengjie/domain/LocalStorage.java | 73 + .../java/me/zhengjie/domain/QiniuConfig.java | 69 + .../java/me/zhengjie/domain/QiniuContent.java | 64 + .../java/me/zhengjie/domain/vo/EmailVo.java | 44 + .../java/me/zhengjie/domain/vo/TradeVo.java | 63 + .../zhengjie/repository/AliPayRepository.java | 26 + .../zhengjie/repository/EmailRepository.java | 26 + .../repository/LocalStorageRepository.java | 27 + .../repository/QiNiuConfigRepository.java | 36 + .../repository/QiniuContentRepository.java | 34 + .../me/zhengjie/rest/AliPayController.java | 135 ++ .../me/zhengjie/rest/EmailController.java | 63 + .../zhengjie/rest/LocalStorageController.java | 100 ++ .../me/zhengjie/rest/QiniuController.java | 122 ++ .../me/zhengjie/service/AliPayService.java | 57 + .../me/zhengjie/service/EmailService.java | 48 + .../zhengjie/service/LocalStorageService.java | 83 ++ .../me/zhengjie/service/QiNiuService.java | 119 ++ .../zhengjie/service/dto/LocalStorageDto.java | 42 + .../dto/LocalStorageQueryCriteria.java | 36 + .../service/dto/PictureQueryCriteria.java | 40 + .../service/dto/QiniuQueryCriteria.java | 36 + .../service/impl/AliPayServiceImpl.java | 119 ++ .../service/impl/EmailServiceImpl.java | 107 ++ .../service/impl/LocalStorageServiceImpl.java | 133 ++ .../service/impl/QiNiuServiceImpl.java | 234 ++++ .../service/mapstruct/LocalStorageMapper.java | 31 + .../me/zhengjie/utils/AliPayStatusEnum.java | 46 + .../java/me/zhengjie/utils/AlipayUtils.java | 85 ++ .../java/me/zhengjie/utils/QiNiuUtil.java | 71 + eladmin-web/.editorconfig | 14 + eladmin-web/.env.development | 8 + eladmin-web/.env.production | 7 + eladmin-web/.eslintignore | 4 + eladmin-web/.eslintrc.js | 198 +++ eladmin-web/.gitignore | 24 + eladmin-web/.travis.yml | 5 + eladmin-web/LICENSE | 191 +++ eladmin-web/README.md | 77 ++ eladmin-web/babel.config.js | 11 + eladmin-web/jest.config.js | 24 + eladmin-web/package.json | 115 ++ eladmin-web/plopfile.js | 7 + eladmin-web/postcss.config.js | 5 + eladmin-web/public/favicon.ico | Bin 0 -> 67646 bytes eladmin-web/public/index.html | 15 + eladmin-web/public/robots.txt | 2 + eladmin-web/src/App.vue | 11 + eladmin-web/src/api/data.js | 17 + eladmin-web/src/api/generator/genConfig.js | 16 + eladmin-web/src/api/generator/generator.js | 33 + eladmin-web/src/api/login.js | 35 + eladmin-web/src/api/mnt/app.js | 27 + eladmin-web/src/api/mnt/connect.js | 17 + eladmin-web/src/api/mnt/database.js | 35 + eladmin-web/src/api/mnt/deploy.js | 77 ++ eladmin-web/src/api/mnt/deployHistory.js | 21 + eladmin-web/src/api/mnt/serverDeploy.js | 27 + eladmin-web/src/api/monitor/log.js | 22 + eladmin-web/src/api/monitor/online.js | 9 + eladmin-web/src/api/system/code.js | 15 + eladmin-web/src/api/system/dept.js | 45 + eladmin-web/src/api/system/dict.js | 34 + eladmin-web/src/api/system/dictDetail.js | 52 + eladmin-web/src/api/system/job.js | 40 + eladmin-web/src/api/system/menu.js | 65 + eladmin-web/src/api/system/purchase.js | 27 + eladmin-web/src/api/system/reimburse.js | 27 + eladmin-web/src/api/system/role.js | 57 + eladmin-web/src/api/system/timing.js | 41 + eladmin-web/src/api/system/user.js | 69 + eladmin-web/src/api/tools/alipay.js | 25 + eladmin-web/src/api/tools/email.js | 24 + eladmin-web/src/api/tools/localStorage.js | 27 + eladmin-web/src/api/tools/qiniu.js | 40 + eladmin-web/src/api/userBank.js | 27 + eladmin-web/src/assets/401_images/401.gif | Bin 0 -> 164227 bytes eladmin-web/src/assets/404_images/404.png | Bin 0 -> 98071 bytes .../src/assets/404_images/404_cloud.png | Bin 0 -> 4766 bytes eladmin-web/src/assets/icons/index.js | 9 + .../src/assets/icons/svg/Steve-Jobs.svg | 1 + eladmin-web/src/assets/icons/svg/alipay.svg | 1 + eladmin-web/src/assets/icons/svg/anq.svg | 1 + eladmin-web/src/assets/icons/svg/app.svg | 1 + eladmin-web/src/assets/icons/svg/backup.svg | 1 + eladmin-web/src/assets/icons/svg/blog.svg | 1 + eladmin-web/src/assets/icons/svg/chain.svg | 1 + eladmin-web/src/assets/icons/svg/chart.svg | 1 + .../src/assets/icons/svg/codeConsole.svg | 1 + .../src/assets/icons/svg/dashboard.svg | 1 + eladmin-web/src/assets/icons/svg/database.svg | 1 + eladmin-web/src/assets/icons/svg/date.svg | 1 + eladmin-web/src/assets/icons/svg/deploy.svg | 1 + eladmin-web/src/assets/icons/svg/dept.svg | 1 + eladmin-web/src/assets/icons/svg/dev.svg | 1 + eladmin-web/src/assets/icons/svg/develop.svg | 1 + .../src/assets/icons/svg/dictionary.svg | 1 + eladmin-web/src/assets/icons/svg/doc.svg | 1 + eladmin-web/src/assets/icons/svg/download.svg | 1 + eladmin-web/src/assets/icons/svg/edit.svg | 1 + .../src/assets/icons/svg/education.svg | 1 + eladmin-web/src/assets/icons/svg/email.svg | 1 + eladmin-web/src/assets/icons/svg/error.svg | 1 + .../src/assets/icons/svg/exit-fullscreen.svg | 1 + .../src/assets/icons/svg/fullscreen.svg | 1 + eladmin-web/src/assets/icons/svg/fwb.svg | 1 + eladmin-web/src/assets/icons/svg/github.svg | 1 + eladmin-web/src/assets/icons/svg/gonggao.svg | 1 + eladmin-web/src/assets/icons/svg/icon.svg | 1 + eladmin-web/src/assets/icons/svg/image.svg | 1 + eladmin-web/src/assets/icons/svg/index.svg | 1 + .../src/assets/icons/svg/international.svg | 1 + eladmin-web/src/assets/icons/svg/ipvisits.svg | 1 + eladmin-web/src/assets/icons/svg/java.svg | 1 + eladmin-web/src/assets/icons/svg/link.svg | 1 + eladmin-web/src/assets/icons/svg/list.svg | 1 + eladmin-web/src/assets/icons/svg/lock.svg | 1 + eladmin-web/src/assets/icons/svg/log.svg | 1 + eladmin-web/src/assets/icons/svg/login.svg | 1 + eladmin-web/src/assets/icons/svg/markdown.svg | 1 + eladmin-web/src/assets/icons/svg/menu.svg | 1 + eladmin-web/src/assets/icons/svg/message.svg | 1 + eladmin-web/src/assets/icons/svg/mnt.svg | 1 + eladmin-web/src/assets/icons/svg/money.svg | 1 + eladmin-web/src/assets/icons/svg/monitor.svg | 1 + eladmin-web/src/assets/icons/svg/nested.svg | 1 + eladmin-web/src/assets/icons/svg/password.svg | 1 + eladmin-web/src/assets/icons/svg/people.svg | 1 + eladmin-web/src/assets/icons/svg/peoples.svg | 1 + .../src/assets/icons/svg/permission.svg | 1 + eladmin-web/src/assets/icons/svg/phone.svg | 1 + eladmin-web/src/assets/icons/svg/qiniu.svg | 1 + eladmin-web/src/assets/icons/svg/redis.svg | 1 + eladmin-web/src/assets/icons/svg/role.svg | 1 + eladmin-web/src/assets/icons/svg/search.svg | 1 + eladmin-web/src/assets/icons/svg/server.svg | 1 + eladmin-web/src/assets/icons/svg/shopping.svg | 1 + eladmin-web/src/assets/icons/svg/size.svg | 1 + eladmin-web/src/assets/icons/svg/skill.svg | 1 + eladmin-web/src/assets/icons/svg/source.svg | 1 + .../src/assets/icons/svg/sqlMonitor.svg | 1 + eladmin-web/src/assets/icons/svg/swagger.svg | 1 + .../src/assets/icons/svg/sys-tools.svg | 1 + eladmin-web/src/assets/icons/svg/system.svg | 1 + eladmin-web/src/assets/icons/svg/system1.svg | 1 + eladmin-web/src/assets/icons/svg/tab.svg | 1 + eladmin-web/src/assets/icons/svg/theme.svg | 1 + eladmin-web/src/assets/icons/svg/timing.svg | 1 + eladmin-web/src/assets/icons/svg/tools.svg | 1 + .../src/assets/icons/svg/tree-table.svg | 1 + eladmin-web/src/assets/icons/svg/tree.svg | 1 + eladmin-web/src/assets/icons/svg/unlock.svg | 1 + eladmin-web/src/assets/icons/svg/user.svg | 1 + eladmin-web/src/assets/icons/svg/user1.svg | 1 + .../src/assets/icons/svg/validCode.svg | 1 + eladmin-web/src/assets/icons/svg/visits.svg | 1 + eladmin-web/src/assets/icons/svg/web.svg | 1 + eladmin-web/src/assets/icons/svg/wechat.svg | 1 + eladmin-web/src/assets/icons/svg/weixin.svg | 1 + eladmin-web/src/assets/icons/svg/zujian.svg | 1 + eladmin-web/src/assets/icons/svgo.yml | 22 + eladmin-web/src/assets/images/avatar.png | Bin 0 -> 1865 bytes eladmin-web/src/assets/images/background.webp | Bin 0 -> 156066 bytes eladmin-web/src/assets/images/logo.png | Bin 0 -> 8852 bytes eladmin-web/src/assets/styles/btn.scss | 99 ++ eladmin-web/src/assets/styles/eladmin.scss | 117 ++ eladmin-web/src/assets/styles/element-ui.scss | 79 ++ .../src/assets/styles/element-variables.scss | 31 + eladmin-web/src/assets/styles/index.scss | 182 +++ eladmin-web/src/assets/styles/mixin.scss | 66 + eladmin-web/src/assets/styles/sidebar.scss | 209 +++ eladmin-web/src/assets/styles/transition.scss | 48 + eladmin-web/src/assets/styles/variables.scss | 35 + .../src/components/Breadcrumb/index.vue | 81 ++ .../src/components/Crud/CRUD.operation.vue | 268 ++++ .../src/components/Crud/Pagination.vue | 18 + .../src/components/Crud/RR.operation.vue | 20 + .../src/components/Crud/UD.operation.vue | 71 + eladmin-web/src/components/Crud/crud.js | 864 ++++++++++++ .../src/components/DateRangePicker/index.vue | 45 + eladmin-web/src/components/Dict/Dict.js | 29 + eladmin-web/src/components/Dict/index.js | 29 + eladmin-web/src/components/Doc/index.vue | 16 + .../src/components/Echarts/BarChart.vue | 106 ++ .../src/components/Echarts/Category.vue | 438 ++++++ eladmin-web/src/components/Echarts/Funnel.vue | 120 ++ eladmin-web/src/components/Echarts/Gauge.vue | 74 + eladmin-web/src/components/Echarts/Graph.vue | 101 ++ .../src/components/Echarts/HeatMap.vue | 105 ++ .../src/components/Echarts/PieChart.vue | 84 ++ eladmin-web/src/components/Echarts/Point.vue | 149 ++ .../src/components/Echarts/RadarChart.vue | 120 ++ eladmin-web/src/components/Echarts/Rich.vue | 149 ++ eladmin-web/src/components/Echarts/Sankey.vue | 100 ++ .../src/components/Echarts/Scatter.vue | 143 ++ .../src/components/Echarts/Sunburst.vue | 107 ++ .../src/components/Echarts/ThemeRiver.vue | 148 ++ .../src/components/GithubCorner/index.vue | 54 + .../src/components/Hamburger/index.vue | 44 + .../src/components/HeaderSearch/index.vue | 188 +++ .../src/components/IconSelect/index.vue | 68 + .../src/components/IconSelect/requireIcons.js | 11 + eladmin-web/src/components/Iframe/index.vue | 30 + eladmin-web/src/components/JavaEdit/index.vue | 77 ++ .../src/components/Pagination/index.vue | 101 ++ eladmin-web/src/components/PanThumb/index.vue | 140 ++ .../src/components/ParentView/index.vue | 3 + .../src/components/Permission/index.js | 13 + .../src/components/Permission/permission.js | 21 + .../src/components/RightPanel/index.vue | 149 ++ .../src/components/Screenfull/index.vue | 60 + .../src/components/SizeSelect/index.vue | 57 + eladmin-web/src/components/SvgIcon/index.vue | 62 + .../src/components/ThemePicker/index.vue | 165 +++ .../src/components/WangEditor/index.vue | 93 ++ eladmin-web/src/components/YamlEdit/index.vue | 81 ++ eladmin-web/src/layout/components/AppMain.vue | 63 + eladmin-web/src/layout/components/Navbar.vue | 200 +++ .../src/layout/components/Settings/index.vue | 108 ++ .../layout/components/Sidebar/FixiOSBug.js | 26 + .../src/layout/components/Sidebar/Item.vue | 29 + .../src/layout/components/Sidebar/Link.vue | 36 + .../src/layout/components/Sidebar/Logo.vue | 82 ++ .../layout/components/Sidebar/SidebarItem.vue | 95 ++ .../src/layout/components/Sidebar/index.vue | 54 + .../layout/components/TagsView/ScrollPane.vue | 85 ++ .../src/layout/components/TagsView/index.vue | 286 ++++ eladmin-web/src/layout/components/index.js | 5 + eladmin-web/src/layout/index.vue | 116 ++ eladmin-web/src/layout/mixin/ResizeHandler.js | 45 + eladmin-web/src/main.js | 41 + eladmin-web/src/mixins/crud.js | 342 +++++ eladmin-web/src/router/index.js | 72 + eladmin-web/src/router/routers.js | 68 + eladmin-web/src/settings.js | 46 + eladmin-web/src/store/getters.js | 24 + eladmin-web/src/store/index.js | 25 + eladmin-web/src/store/modules/api.js | 26 + eladmin-web/src/store/modules/app.js | 56 + eladmin-web/src/store/modules/permission.js | 83 ++ eladmin-web/src/store/modules/settings.js | 36 + eladmin-web/src/store/modules/tagsView.js | 165 +++ eladmin-web/src/store/modules/user.js | 94 ++ eladmin-web/src/utils/auth.js | 18 + eladmin-web/src/utils/clipboard.js | 36 + eladmin-web/src/utils/datetime.js | 216 +++ eladmin-web/src/utils/index.js | 388 ++++++ eladmin-web/src/utils/permission.js | 23 + eladmin-web/src/utils/request.js | 88 ++ eladmin-web/src/utils/rsaEncrypt.js | 14 + eladmin-web/src/utils/shortcuts.js | 76 ++ eladmin-web/src/utils/upload.js | 11 + eladmin-web/src/utils/validate.js | 167 +++ eladmin-web/src/views/components/Echarts.vue | 110 ++ eladmin-web/src/views/components/Editor.vue | 35 + eladmin-web/src/views/components/MarkDown.vue | 55 + eladmin-web/src/views/components/YamlEdit.vue | 207 +++ .../views/components/icons/element-icons.js | 74 + .../src/views/components/icons/index.vue | 97 ++ .../src/views/components/icons/svg-icons.js | 10 + eladmin-web/src/views/dashboard/LineChart.vue | 135 ++ .../src/views/dashboard/PanelGroup.vue | 181 +++ .../src/views/dashboard/mixins/resize.js | 55 + eladmin-web/src/views/features/401.vue | 89 ++ eladmin-web/src/views/features/404.vue | 225 +++ eladmin-web/src/views/features/redirect.vue | 12 + eladmin-web/src/views/generator/config.vue | 326 +++++ eladmin-web/src/views/generator/index.vue | 114 ++ eladmin-web/src/views/generator/preview.vue | 30 + eladmin-web/src/views/home.vue | 107 ++ eladmin-web/src/views/login.vue | 214 +++ eladmin-web/src/views/mnt/app/index.vue | 144 ++ .../src/views/mnt/database/execute.vue | 86 ++ eladmin-web/src/views/mnt/database/index.vue | 148 ++ eladmin-web/src/views/mnt/deploy/deploy.vue | 190 +++ eladmin-web/src/views/mnt/deploy/index.vue | 229 ++++ .../src/views/mnt/deploy/sysRestore.vue | 108 ++ .../src/views/mnt/deployHistory/index.vue | 93 ++ eladmin-web/src/views/mnt/server/index.vue | 136 ++ .../src/views/monitor/log/errorLog.vue | 135 ++ eladmin-web/src/views/monitor/log/index.vue | 114 ++ eladmin-web/src/views/monitor/log/search.vue | 24 + .../src/views/monitor/online/index.vue | 121 ++ .../src/views/monitor/server/index.vue | 291 ++++ eladmin-web/src/views/monitor/sql/index.vue | 16 + .../src/views/nested/menu1/menu1-1/index.vue | 36 + .../src/views/nested/menu1/menu1-2/index.vue | 5 + eladmin-web/src/views/nested/menu2/index.vue | 5 + eladmin-web/src/views/system/dept/index.vue | 254 ++++ .../src/views/system/dict/dictDetail.vue | 115 ++ eladmin-web/src/views/system/dict/index.vue | 135 ++ eladmin-web/src/views/system/job/index.vue | 110 ++ .../src/views/system/job/module/form.vue | 110 ++ .../src/views/system/job/module/header.vue | 32 + eladmin-web/src/views/system/menu/index.vue | 252 ++++ .../src/views/system/purchase/index.vue | 179 +++ .../src/views/system/reimburse/index.vue | 194 +++ eladmin-web/src/views/system/role/index.vue | 364 +++++ eladmin-web/src/views/system/timing/index.vue | 210 +++ eladmin-web/src/views/system/timing/log.vue | 104 ++ eladmin-web/src/views/system/user/center.vue | 225 +++ .../views/system/user/center/updateEmail.vue | 137 ++ .../views/system/user/center/updatePass.vue | 95 ++ eladmin-web/src/views/system/user/index.vue | 513 +++++++ .../src/views/system/userbank/index.vue | 116 ++ eladmin-web/src/views/tools/aliPay/config.vue | 98 ++ eladmin-web/src/views/tools/aliPay/index.vue | 48 + eladmin-web/src/views/tools/aliPay/toPay.vue | 86 ++ eladmin-web/src/views/tools/email/config.vue | 91 ++ eladmin-web/src/views/tools/email/index.vue | 41 + eladmin-web/src/views/tools/email/send.vue | 98 ++ eladmin-web/src/views/tools/storage/index.vue | 36 + .../src/views/tools/storage/local/index.vue | 184 +++ .../src/views/tools/storage/qiniu/form.vue | 98 ++ .../src/views/tools/storage/qiniu/index.vue | 189 +++ eladmin-web/src/views/tools/swagger/index.vue | 16 + eladmin-web/vue.config.js | 147 ++ pom.xml | 230 ++++ sql/eladmin.sql | 824 +++++++++++ sql/finance.sql | 1216 +++++++++++++++++ sql/脚本如何选择.md | 2 + 618 files changed, 47423 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 eladmin-common/pom.xml create mode 100644 eladmin-common/src/main/java/me/zhengjie/annotation/AnonymousAccess.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/annotation/DataPermission.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/annotation/Limit.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/annotation/Query.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/annotation/rest/AnonymousDeleteMapping.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/annotation/rest/AnonymousGetMapping.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/annotation/rest/AnonymousPatchMapping.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/annotation/rest/AnonymousPostMapping.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/annotation/rest/AnonymousPutMapping.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/aspect/LimitAspect.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/aspect/LimitType.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/base/BaseDTO.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/base/BaseEntity.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/base/BaseMapper.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/config/AuditorConfig.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/config/AuthorityConfig.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/config/FileProperties.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/config/RedisConfig.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/config/RsaProperties.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/config/SwaggerConfig.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/exception/BadConfigurationException.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/exception/BadRequestException.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/exception/EntityExistException.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/exception/EntityNotFoundException.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/exception/handler/ApiError.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/exception/handler/GlobalExceptionHandler.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/utils/CacheKey.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/utils/CallBack.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/utils/CloseUtil.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/utils/DateUtil.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/utils/ElConstant.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/utils/EncryptUtils.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/utils/FileUtil.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/utils/PageResult.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/utils/PageUtil.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/utils/QueryHelp.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/utils/RedisUtils.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/utils/RequestHolder.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/utils/RsaUtils.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/utils/SecurityUtils.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/utils/SpringContextHolder.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/utils/StringUtils.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/utils/ThrowableUtil.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/utils/ValidationUtil.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/utils/enums/CodeBiEnum.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/utils/enums/CodeEnum.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/utils/enums/DataScopeEnum.java create mode 100644 eladmin-common/src/main/java/me/zhengjie/utils/enums/RequestMethodEnum.java create mode 100644 eladmin-common/src/test/java/me/zhengjie/utils/DateUtilsTest.java create mode 100644 eladmin-common/src/test/java/me/zhengjie/utils/EncryptUtilsTest.java create mode 100644 eladmin-common/src/test/java/me/zhengjie/utils/FileUtilTest.java create mode 100644 eladmin-common/src/test/java/me/zhengjie/utils/StringUtilsTest.java create mode 100644 eladmin-generator/pom.xml create mode 100644 eladmin-generator/src/main/java/me/zhengjie/domain/ColumnInfo.java create mode 100644 eladmin-generator/src/main/java/me/zhengjie/domain/GenConfig.java create mode 100644 eladmin-generator/src/main/java/me/zhengjie/domain/vo/TableInfo.java create mode 100644 eladmin-generator/src/main/java/me/zhengjie/repository/ColumnInfoRepository.java create mode 100644 eladmin-generator/src/main/java/me/zhengjie/repository/GenConfigRepository.java create mode 100644 eladmin-generator/src/main/java/me/zhengjie/rest/GenConfigController.java create mode 100644 eladmin-generator/src/main/java/me/zhengjie/rest/GeneratorController.java create mode 100644 eladmin-generator/src/main/java/me/zhengjie/service/GenConfigService.java create mode 100644 eladmin-generator/src/main/java/me/zhengjie/service/GeneratorService.java create mode 100644 eladmin-generator/src/main/java/me/zhengjie/service/impl/GenConfigServiceImpl.java create mode 100644 eladmin-generator/src/main/java/me/zhengjie/service/impl/GeneratorServiceImpl.java create mode 100644 eladmin-generator/src/main/java/me/zhengjie/utils/ColUtil.java create mode 100644 eladmin-generator/src/main/java/me/zhengjie/utils/GenUtil.java create mode 100644 eladmin-generator/src/main/resources/generator.properties create mode 100644 eladmin-generator/src/main/resources/template/admin/Controller.ftl create mode 100644 eladmin-generator/src/main/resources/template/admin/Dto.ftl create mode 100644 eladmin-generator/src/main/resources/template/admin/Entity.ftl create mode 100644 eladmin-generator/src/main/resources/template/admin/Mapper.ftl create mode 100644 eladmin-generator/src/main/resources/template/admin/QueryCriteria.ftl create mode 100644 eladmin-generator/src/main/resources/template/admin/Repository.ftl create mode 100644 eladmin-generator/src/main/resources/template/admin/Service.ftl create mode 100644 eladmin-generator/src/main/resources/template/admin/ServiceImpl.ftl create mode 100644 eladmin-generator/src/main/resources/template/front/api.ftl create mode 100644 eladmin-generator/src/main/resources/template/front/index.ftl create mode 100644 eladmin-logging/pom.xml create mode 100644 eladmin-logging/src/main/java/me/zhengjie/annotation/Log.java create mode 100644 eladmin-logging/src/main/java/me/zhengjie/aspect/LogAspect.java create mode 100644 eladmin-logging/src/main/java/me/zhengjie/domain/SysLog.java create mode 100644 eladmin-logging/src/main/java/me/zhengjie/repository/LogRepository.java create mode 100644 eladmin-logging/src/main/java/me/zhengjie/rest/SysLogController.java create mode 100644 eladmin-logging/src/main/java/me/zhengjie/service/SysLogService.java create mode 100644 eladmin-logging/src/main/java/me/zhengjie/service/dto/SysLogErrorDto.java create mode 100644 eladmin-logging/src/main/java/me/zhengjie/service/dto/SysLogQueryCriteria.java create mode 100644 eladmin-logging/src/main/java/me/zhengjie/service/dto/SysLogSmallDto.java create mode 100644 eladmin-logging/src/main/java/me/zhengjie/service/impl/SysLogServiceImpl.java create mode 100644 eladmin-logging/src/main/java/me/zhengjie/service/mapstruct/LogErrorMapper.java create mode 100644 eladmin-logging/src/main/java/me/zhengjie/service/mapstruct/LogSmallMapper.java create mode 100644 eladmin-system/pom.xml create mode 100644 eladmin-system/src/main/java/me/zhengjie/AppRun.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/config/ConfigurerAdapter.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/config/RelaxedQueryCharsConnectorCustomizer.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/config/WebSocketConfig.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/config/thread/AsyncTaskProperties.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/config/thread/CustomExecutorConfig.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/domain/App.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/domain/Database.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/domain/Deploy.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/domain/DeployHistory.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/domain/ServerDeploy.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/repository/AppRepository.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/repository/DatabaseRepository.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/repository/DeployHistoryRepository.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/repository/DeployRepository.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/repository/ServerDeployRepository.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/rest/AppController.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/rest/DatabaseController.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/rest/DeployController.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/rest/DeployHistoryController.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/rest/ServerDeployController.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/AppService.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/DatabaseService.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/DeployHistoryService.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/DeployService.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/ServerDeployService.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/AppDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/AppQueryCriteria.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DatabaseDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DatabaseQueryCriteria.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DeployDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DeployHistoryDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DeployHistoryQueryCriteria.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DeployQueryCriteria.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/ServerDeployDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/ServerDeployQueryCriteria.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/impl/AppServiceImpl.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/impl/DatabaseServiceImpl.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/impl/DeployHistoryServiceImpl.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/impl/DeployServiceImpl.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/impl/ServerDeployServiceImpl.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/mapstruct/AppMapper.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/mapstruct/DatabaseMapper.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/mapstruct/DeployHistoryMapper.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/mapstruct/DeployMapper.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/mapstruct/ServerDeployMapper.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/util/DataTypeEnum.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/util/ExecuteShellUtil.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/util/ScpClientUtil.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/util/SqlUtils.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/websocket/MsgType.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/websocket/SocketMsg.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/mnt/websocket/WebSocketServer.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/quartz/config/JobRunner.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/quartz/config/QuartzConfig.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/quartz/domain/QuartzJob.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/quartz/domain/QuartzLog.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/quartz/repository/QuartzJobRepository.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/quartz/repository/QuartzLogRepository.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/quartz/rest/QuartzJobController.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/quartz/service/QuartzJobService.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/quartz/service/dto/JobQueryCriteria.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/quartz/service/impl/QuartzJobServiceImpl.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/quartz/task/TestTask.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/quartz/utils/ExecutionJob.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/quartz/utils/QuartzManage.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/quartz/utils/QuartzRunnable.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/security/config/ConfigBeanConfiguration.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/security/config/SpringSecurityConfig.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/security/config/bean/LoginCode.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/security/config/bean/LoginCodeEnum.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/security/config/bean/LoginProperties.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/security/config/bean/SecurityProperties.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/security/rest/AuthorizationController.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/security/rest/OnlineController.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/security/security/JwtAccessDeniedHandler.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/security/security/JwtAuthenticationEntryPoint.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/security/security/TokenConfigurer.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/security/security/TokenFilter.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/security/security/TokenProvider.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/security/service/OnlineUserService.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/security/service/UserCacheManager.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/security/service/UserDetailsServiceImpl.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/security/service/dto/AuthUserDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/security/service/dto/AuthorityDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/security/service/dto/JwtUserDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/security/service/dto/OnlineUserDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Dept.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Dict.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/domain/DictDetail.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Job.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Menu.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Purchase.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Reimburse.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Role.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/domain/User.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/domain/UserBank.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/domain/vo/MenuMetaVo.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/domain/vo/MenuVo.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/domain/vo/UserPassVo.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/repository/DeptRepository.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/repository/DictDetailRepository.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/repository/DictRepository.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/repository/JobRepository.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/repository/MenuRepository.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/repository/PurchaseRepository.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/repository/ReimburseRepository.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/repository/RoleRepository.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/repository/UserBankRepository.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/repository/UserRepository.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/rest/DeptController.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/rest/DictController.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/rest/DictDetailController.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/rest/JobController.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/rest/LimitController.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/rest/MenuController.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/rest/MonitorController.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/rest/PurchaseController.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/rest/ReimburseController.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/rest/RoleController.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/rest/UserBankController.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/rest/UserController.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/rest/VerifyController.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/DataService.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/DeptService.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/DictDetailService.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/DictService.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/JobService.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/MenuService.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/MonitorService.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/PurchaseService.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/ReimburseService.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/RoleService.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/UserBankService.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/UserService.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/VerifyService.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DeptDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DeptQueryCriteria.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DeptSmallDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DictDetailDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DictDetailQueryCriteria.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DictDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DictQueryCriteria.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DictSmallDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/JobDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/JobQueryCriteria.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/JobSmallDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/MenuDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/MenuQueryCriteria.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/PurchaseDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/PurchaseQueryCriteria.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/ReimburseDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/ReimburseQueryCriteria.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/RoleDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/RoleQueryCriteria.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/RoleSmallDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/UserBankDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/UserBankQueryCriteria.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/UserDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/UserLoginDto.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/UserQueryCriteria.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/DataServiceImpl.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/DeptServiceImpl.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/DictDetailServiceImpl.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/DictServiceImpl.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/JobServiceImpl.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/MenuServiceImpl.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/MonitorServiceImpl.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/PurchaseServiceImpl.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/ReimburseServiceImpl.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/RoleServiceImpl.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/UserBankServiceImpl.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/UserServiceImpl.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/VerifyServiceImpl.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/DeptMapper.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/DeptSmallMapper.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/DictDetailMapper.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/DictMapper.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/DictSmallMapper.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/JobMapper.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/JobSmallMapper.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/MenuMapper.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/PurchaseMapper.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/ReimburseMapper.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/RoleMapper.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/RoleSmallMapper.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/UserBankMapper.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/UserLoginMapper.java create mode 100644 eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/UserMapper.java create mode 100644 eladmin-system/src/main/resources/banner.txt create mode 100644 eladmin-system/src/main/resources/config/application-dev.yml create mode 100644 eladmin-system/src/main/resources/config/application-prod.yml create mode 100644 eladmin-system/src/main/resources/config/application.yml create mode 100644 eladmin-system/src/main/resources/log4jdbc.log4j2.properties create mode 100644 eladmin-system/src/main/resources/logback.xml create mode 100644 eladmin-system/src/main/resources/template/email.ftl create mode 100644 eladmin-system/src/main/resources/template/taskAlarm.ftl create mode 100644 eladmin-system/src/test/java/me/zhengjie/EladminSystemApplicationTests.java create mode 100644 eladmin-tools/pom.xml create mode 100644 eladmin-tools/src/main/java/me/zhengjie/config/MultipartConfig.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/domain/AlipayConfig.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/domain/EmailConfig.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/domain/LocalStorage.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/domain/QiniuConfig.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/domain/QiniuContent.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/domain/vo/EmailVo.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/domain/vo/TradeVo.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/repository/AliPayRepository.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/repository/EmailRepository.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/repository/LocalStorageRepository.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/repository/QiNiuConfigRepository.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/repository/QiniuContentRepository.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/rest/AliPayController.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/rest/EmailController.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/rest/LocalStorageController.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/rest/QiniuController.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/service/AliPayService.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/service/EmailService.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/service/LocalStorageService.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/service/QiNiuService.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/service/dto/LocalStorageDto.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/service/dto/LocalStorageQueryCriteria.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/service/dto/PictureQueryCriteria.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/service/dto/QiniuQueryCriteria.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/service/impl/AliPayServiceImpl.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/service/impl/EmailServiceImpl.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/service/impl/LocalStorageServiceImpl.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/service/impl/QiNiuServiceImpl.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/service/mapstruct/LocalStorageMapper.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/utils/AliPayStatusEnum.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/utils/AlipayUtils.java create mode 100644 eladmin-tools/src/main/java/me/zhengjie/utils/QiNiuUtil.java create mode 100644 eladmin-web/.editorconfig create mode 100644 eladmin-web/.env.development create mode 100644 eladmin-web/.env.production create mode 100644 eladmin-web/.eslintignore create mode 100644 eladmin-web/.eslintrc.js create mode 100644 eladmin-web/.gitignore create mode 100644 eladmin-web/.travis.yml create mode 100644 eladmin-web/LICENSE create mode 100644 eladmin-web/README.md create mode 100644 eladmin-web/babel.config.js create mode 100644 eladmin-web/jest.config.js create mode 100644 eladmin-web/package.json create mode 100644 eladmin-web/plopfile.js create mode 100644 eladmin-web/postcss.config.js create mode 100644 eladmin-web/public/favicon.ico create mode 100644 eladmin-web/public/index.html create mode 100644 eladmin-web/public/robots.txt create mode 100644 eladmin-web/src/App.vue create mode 100644 eladmin-web/src/api/data.js create mode 100644 eladmin-web/src/api/generator/genConfig.js create mode 100644 eladmin-web/src/api/generator/generator.js create mode 100644 eladmin-web/src/api/login.js create mode 100644 eladmin-web/src/api/mnt/app.js create mode 100644 eladmin-web/src/api/mnt/connect.js create mode 100644 eladmin-web/src/api/mnt/database.js create mode 100644 eladmin-web/src/api/mnt/deploy.js create mode 100644 eladmin-web/src/api/mnt/deployHistory.js create mode 100644 eladmin-web/src/api/mnt/serverDeploy.js create mode 100644 eladmin-web/src/api/monitor/log.js create mode 100644 eladmin-web/src/api/monitor/online.js create mode 100644 eladmin-web/src/api/system/code.js create mode 100644 eladmin-web/src/api/system/dept.js create mode 100644 eladmin-web/src/api/system/dict.js create mode 100644 eladmin-web/src/api/system/dictDetail.js create mode 100644 eladmin-web/src/api/system/job.js create mode 100644 eladmin-web/src/api/system/menu.js create mode 100644 eladmin-web/src/api/system/purchase.js create mode 100644 eladmin-web/src/api/system/reimburse.js create mode 100644 eladmin-web/src/api/system/role.js create mode 100644 eladmin-web/src/api/system/timing.js create mode 100644 eladmin-web/src/api/system/user.js create mode 100644 eladmin-web/src/api/tools/alipay.js create mode 100644 eladmin-web/src/api/tools/email.js create mode 100644 eladmin-web/src/api/tools/localStorage.js create mode 100644 eladmin-web/src/api/tools/qiniu.js create mode 100644 eladmin-web/src/api/userBank.js create mode 100644 eladmin-web/src/assets/401_images/401.gif create mode 100644 eladmin-web/src/assets/404_images/404.png create mode 100644 eladmin-web/src/assets/404_images/404_cloud.png create mode 100644 eladmin-web/src/assets/icons/index.js create mode 100644 eladmin-web/src/assets/icons/svg/Steve-Jobs.svg create mode 100644 eladmin-web/src/assets/icons/svg/alipay.svg create mode 100644 eladmin-web/src/assets/icons/svg/anq.svg create mode 100644 eladmin-web/src/assets/icons/svg/app.svg create mode 100644 eladmin-web/src/assets/icons/svg/backup.svg create mode 100644 eladmin-web/src/assets/icons/svg/blog.svg create mode 100644 eladmin-web/src/assets/icons/svg/chain.svg create mode 100644 eladmin-web/src/assets/icons/svg/chart.svg create mode 100644 eladmin-web/src/assets/icons/svg/codeConsole.svg create mode 100644 eladmin-web/src/assets/icons/svg/dashboard.svg create mode 100644 eladmin-web/src/assets/icons/svg/database.svg create mode 100644 eladmin-web/src/assets/icons/svg/date.svg create mode 100644 eladmin-web/src/assets/icons/svg/deploy.svg create mode 100644 eladmin-web/src/assets/icons/svg/dept.svg create mode 100644 eladmin-web/src/assets/icons/svg/dev.svg create mode 100644 eladmin-web/src/assets/icons/svg/develop.svg create mode 100644 eladmin-web/src/assets/icons/svg/dictionary.svg create mode 100644 eladmin-web/src/assets/icons/svg/doc.svg create mode 100644 eladmin-web/src/assets/icons/svg/download.svg create mode 100644 eladmin-web/src/assets/icons/svg/edit.svg create mode 100644 eladmin-web/src/assets/icons/svg/education.svg create mode 100644 eladmin-web/src/assets/icons/svg/email.svg create mode 100644 eladmin-web/src/assets/icons/svg/error.svg create mode 100644 eladmin-web/src/assets/icons/svg/exit-fullscreen.svg create mode 100644 eladmin-web/src/assets/icons/svg/fullscreen.svg create mode 100644 eladmin-web/src/assets/icons/svg/fwb.svg create mode 100644 eladmin-web/src/assets/icons/svg/github.svg create mode 100644 eladmin-web/src/assets/icons/svg/gonggao.svg create mode 100644 eladmin-web/src/assets/icons/svg/icon.svg create mode 100644 eladmin-web/src/assets/icons/svg/image.svg create mode 100644 eladmin-web/src/assets/icons/svg/index.svg create mode 100644 eladmin-web/src/assets/icons/svg/international.svg create mode 100644 eladmin-web/src/assets/icons/svg/ipvisits.svg create mode 100644 eladmin-web/src/assets/icons/svg/java.svg create mode 100644 eladmin-web/src/assets/icons/svg/link.svg create mode 100644 eladmin-web/src/assets/icons/svg/list.svg create mode 100644 eladmin-web/src/assets/icons/svg/lock.svg create mode 100644 eladmin-web/src/assets/icons/svg/log.svg create mode 100644 eladmin-web/src/assets/icons/svg/login.svg create mode 100644 eladmin-web/src/assets/icons/svg/markdown.svg create mode 100644 eladmin-web/src/assets/icons/svg/menu.svg create mode 100644 eladmin-web/src/assets/icons/svg/message.svg create mode 100644 eladmin-web/src/assets/icons/svg/mnt.svg create mode 100644 eladmin-web/src/assets/icons/svg/money.svg create mode 100644 eladmin-web/src/assets/icons/svg/monitor.svg create mode 100644 eladmin-web/src/assets/icons/svg/nested.svg create mode 100644 eladmin-web/src/assets/icons/svg/password.svg create mode 100644 eladmin-web/src/assets/icons/svg/people.svg create mode 100644 eladmin-web/src/assets/icons/svg/peoples.svg create mode 100644 eladmin-web/src/assets/icons/svg/permission.svg create mode 100644 eladmin-web/src/assets/icons/svg/phone.svg create mode 100644 eladmin-web/src/assets/icons/svg/qiniu.svg create mode 100644 eladmin-web/src/assets/icons/svg/redis.svg create mode 100644 eladmin-web/src/assets/icons/svg/role.svg create mode 100644 eladmin-web/src/assets/icons/svg/search.svg create mode 100644 eladmin-web/src/assets/icons/svg/server.svg create mode 100644 eladmin-web/src/assets/icons/svg/shopping.svg create mode 100644 eladmin-web/src/assets/icons/svg/size.svg create mode 100644 eladmin-web/src/assets/icons/svg/skill.svg create mode 100644 eladmin-web/src/assets/icons/svg/source.svg create mode 100644 eladmin-web/src/assets/icons/svg/sqlMonitor.svg create mode 100644 eladmin-web/src/assets/icons/svg/swagger.svg create mode 100644 eladmin-web/src/assets/icons/svg/sys-tools.svg create mode 100644 eladmin-web/src/assets/icons/svg/system.svg create mode 100644 eladmin-web/src/assets/icons/svg/system1.svg create mode 100644 eladmin-web/src/assets/icons/svg/tab.svg create mode 100644 eladmin-web/src/assets/icons/svg/theme.svg create mode 100644 eladmin-web/src/assets/icons/svg/timing.svg create mode 100644 eladmin-web/src/assets/icons/svg/tools.svg create mode 100644 eladmin-web/src/assets/icons/svg/tree-table.svg create mode 100644 eladmin-web/src/assets/icons/svg/tree.svg create mode 100644 eladmin-web/src/assets/icons/svg/unlock.svg create mode 100644 eladmin-web/src/assets/icons/svg/user.svg create mode 100644 eladmin-web/src/assets/icons/svg/user1.svg create mode 100644 eladmin-web/src/assets/icons/svg/validCode.svg create mode 100644 eladmin-web/src/assets/icons/svg/visits.svg create mode 100644 eladmin-web/src/assets/icons/svg/web.svg create mode 100644 eladmin-web/src/assets/icons/svg/wechat.svg create mode 100644 eladmin-web/src/assets/icons/svg/weixin.svg create mode 100644 eladmin-web/src/assets/icons/svg/zujian.svg create mode 100644 eladmin-web/src/assets/icons/svgo.yml create mode 100644 eladmin-web/src/assets/images/avatar.png create mode 100644 eladmin-web/src/assets/images/background.webp create mode 100644 eladmin-web/src/assets/images/logo.png create mode 100644 eladmin-web/src/assets/styles/btn.scss create mode 100644 eladmin-web/src/assets/styles/eladmin.scss create mode 100644 eladmin-web/src/assets/styles/element-ui.scss create mode 100644 eladmin-web/src/assets/styles/element-variables.scss create mode 100644 eladmin-web/src/assets/styles/index.scss create mode 100644 eladmin-web/src/assets/styles/mixin.scss create mode 100644 eladmin-web/src/assets/styles/sidebar.scss create mode 100644 eladmin-web/src/assets/styles/transition.scss create mode 100644 eladmin-web/src/assets/styles/variables.scss create mode 100644 eladmin-web/src/components/Breadcrumb/index.vue create mode 100644 eladmin-web/src/components/Crud/CRUD.operation.vue create mode 100644 eladmin-web/src/components/Crud/Pagination.vue create mode 100644 eladmin-web/src/components/Crud/RR.operation.vue create mode 100644 eladmin-web/src/components/Crud/UD.operation.vue create mode 100644 eladmin-web/src/components/Crud/crud.js create mode 100644 eladmin-web/src/components/DateRangePicker/index.vue create mode 100644 eladmin-web/src/components/Dict/Dict.js create mode 100644 eladmin-web/src/components/Dict/index.js create mode 100644 eladmin-web/src/components/Doc/index.vue create mode 100644 eladmin-web/src/components/Echarts/BarChart.vue create mode 100644 eladmin-web/src/components/Echarts/Category.vue create mode 100644 eladmin-web/src/components/Echarts/Funnel.vue create mode 100644 eladmin-web/src/components/Echarts/Gauge.vue create mode 100644 eladmin-web/src/components/Echarts/Graph.vue create mode 100644 eladmin-web/src/components/Echarts/HeatMap.vue create mode 100644 eladmin-web/src/components/Echarts/PieChart.vue create mode 100644 eladmin-web/src/components/Echarts/Point.vue create mode 100644 eladmin-web/src/components/Echarts/RadarChart.vue create mode 100644 eladmin-web/src/components/Echarts/Rich.vue create mode 100644 eladmin-web/src/components/Echarts/Sankey.vue create mode 100644 eladmin-web/src/components/Echarts/Scatter.vue create mode 100644 eladmin-web/src/components/Echarts/Sunburst.vue create mode 100644 eladmin-web/src/components/Echarts/ThemeRiver.vue create mode 100644 eladmin-web/src/components/GithubCorner/index.vue create mode 100644 eladmin-web/src/components/Hamburger/index.vue create mode 100644 eladmin-web/src/components/HeaderSearch/index.vue create mode 100644 eladmin-web/src/components/IconSelect/index.vue create mode 100644 eladmin-web/src/components/IconSelect/requireIcons.js create mode 100644 eladmin-web/src/components/Iframe/index.vue create mode 100644 eladmin-web/src/components/JavaEdit/index.vue create mode 100644 eladmin-web/src/components/Pagination/index.vue create mode 100644 eladmin-web/src/components/PanThumb/index.vue create mode 100644 eladmin-web/src/components/ParentView/index.vue create mode 100644 eladmin-web/src/components/Permission/index.js create mode 100644 eladmin-web/src/components/Permission/permission.js create mode 100644 eladmin-web/src/components/RightPanel/index.vue create mode 100644 eladmin-web/src/components/Screenfull/index.vue create mode 100644 eladmin-web/src/components/SizeSelect/index.vue create mode 100644 eladmin-web/src/components/SvgIcon/index.vue create mode 100644 eladmin-web/src/components/ThemePicker/index.vue create mode 100644 eladmin-web/src/components/WangEditor/index.vue create mode 100644 eladmin-web/src/components/YamlEdit/index.vue create mode 100644 eladmin-web/src/layout/components/AppMain.vue create mode 100644 eladmin-web/src/layout/components/Navbar.vue create mode 100644 eladmin-web/src/layout/components/Settings/index.vue create mode 100644 eladmin-web/src/layout/components/Sidebar/FixiOSBug.js create mode 100644 eladmin-web/src/layout/components/Sidebar/Item.vue create mode 100644 eladmin-web/src/layout/components/Sidebar/Link.vue create mode 100644 eladmin-web/src/layout/components/Sidebar/Logo.vue create mode 100644 eladmin-web/src/layout/components/Sidebar/SidebarItem.vue create mode 100644 eladmin-web/src/layout/components/Sidebar/index.vue create mode 100644 eladmin-web/src/layout/components/TagsView/ScrollPane.vue create mode 100644 eladmin-web/src/layout/components/TagsView/index.vue create mode 100644 eladmin-web/src/layout/components/index.js create mode 100644 eladmin-web/src/layout/index.vue create mode 100644 eladmin-web/src/layout/mixin/ResizeHandler.js create mode 100644 eladmin-web/src/main.js create mode 100644 eladmin-web/src/mixins/crud.js create mode 100644 eladmin-web/src/router/index.js create mode 100644 eladmin-web/src/router/routers.js create mode 100644 eladmin-web/src/settings.js create mode 100644 eladmin-web/src/store/getters.js create mode 100644 eladmin-web/src/store/index.js create mode 100644 eladmin-web/src/store/modules/api.js create mode 100644 eladmin-web/src/store/modules/app.js create mode 100644 eladmin-web/src/store/modules/permission.js create mode 100644 eladmin-web/src/store/modules/settings.js create mode 100644 eladmin-web/src/store/modules/tagsView.js create mode 100644 eladmin-web/src/store/modules/user.js create mode 100644 eladmin-web/src/utils/auth.js create mode 100644 eladmin-web/src/utils/clipboard.js create mode 100644 eladmin-web/src/utils/datetime.js create mode 100644 eladmin-web/src/utils/index.js create mode 100644 eladmin-web/src/utils/permission.js create mode 100644 eladmin-web/src/utils/request.js create mode 100644 eladmin-web/src/utils/rsaEncrypt.js create mode 100644 eladmin-web/src/utils/shortcuts.js create mode 100644 eladmin-web/src/utils/upload.js create mode 100644 eladmin-web/src/utils/validate.js create mode 100644 eladmin-web/src/views/components/Echarts.vue create mode 100644 eladmin-web/src/views/components/Editor.vue create mode 100644 eladmin-web/src/views/components/MarkDown.vue create mode 100644 eladmin-web/src/views/components/YamlEdit.vue create mode 100644 eladmin-web/src/views/components/icons/element-icons.js create mode 100644 eladmin-web/src/views/components/icons/index.vue create mode 100644 eladmin-web/src/views/components/icons/svg-icons.js create mode 100644 eladmin-web/src/views/dashboard/LineChart.vue create mode 100644 eladmin-web/src/views/dashboard/PanelGroup.vue create mode 100644 eladmin-web/src/views/dashboard/mixins/resize.js create mode 100644 eladmin-web/src/views/features/401.vue create mode 100644 eladmin-web/src/views/features/404.vue create mode 100644 eladmin-web/src/views/features/redirect.vue create mode 100644 eladmin-web/src/views/generator/config.vue create mode 100644 eladmin-web/src/views/generator/index.vue create mode 100644 eladmin-web/src/views/generator/preview.vue create mode 100644 eladmin-web/src/views/home.vue create mode 100644 eladmin-web/src/views/login.vue create mode 100644 eladmin-web/src/views/mnt/app/index.vue create mode 100644 eladmin-web/src/views/mnt/database/execute.vue create mode 100644 eladmin-web/src/views/mnt/database/index.vue create mode 100644 eladmin-web/src/views/mnt/deploy/deploy.vue create mode 100644 eladmin-web/src/views/mnt/deploy/index.vue create mode 100644 eladmin-web/src/views/mnt/deploy/sysRestore.vue create mode 100644 eladmin-web/src/views/mnt/deployHistory/index.vue create mode 100644 eladmin-web/src/views/mnt/server/index.vue create mode 100644 eladmin-web/src/views/monitor/log/errorLog.vue create mode 100644 eladmin-web/src/views/monitor/log/index.vue create mode 100644 eladmin-web/src/views/monitor/log/search.vue create mode 100644 eladmin-web/src/views/monitor/online/index.vue create mode 100644 eladmin-web/src/views/monitor/server/index.vue create mode 100644 eladmin-web/src/views/monitor/sql/index.vue create mode 100644 eladmin-web/src/views/nested/menu1/menu1-1/index.vue create mode 100644 eladmin-web/src/views/nested/menu1/menu1-2/index.vue create mode 100644 eladmin-web/src/views/nested/menu2/index.vue create mode 100644 eladmin-web/src/views/system/dept/index.vue create mode 100644 eladmin-web/src/views/system/dict/dictDetail.vue create mode 100644 eladmin-web/src/views/system/dict/index.vue create mode 100644 eladmin-web/src/views/system/job/index.vue create mode 100644 eladmin-web/src/views/system/job/module/form.vue create mode 100644 eladmin-web/src/views/system/job/module/header.vue create mode 100644 eladmin-web/src/views/system/menu/index.vue create mode 100644 eladmin-web/src/views/system/purchase/index.vue create mode 100644 eladmin-web/src/views/system/reimburse/index.vue create mode 100644 eladmin-web/src/views/system/role/index.vue create mode 100644 eladmin-web/src/views/system/timing/index.vue create mode 100644 eladmin-web/src/views/system/timing/log.vue create mode 100644 eladmin-web/src/views/system/user/center.vue create mode 100644 eladmin-web/src/views/system/user/center/updateEmail.vue create mode 100644 eladmin-web/src/views/system/user/center/updatePass.vue create mode 100644 eladmin-web/src/views/system/user/index.vue create mode 100644 eladmin-web/src/views/system/userbank/index.vue create mode 100644 eladmin-web/src/views/tools/aliPay/config.vue create mode 100644 eladmin-web/src/views/tools/aliPay/index.vue create mode 100644 eladmin-web/src/views/tools/aliPay/toPay.vue create mode 100644 eladmin-web/src/views/tools/email/config.vue create mode 100644 eladmin-web/src/views/tools/email/index.vue create mode 100644 eladmin-web/src/views/tools/email/send.vue create mode 100644 eladmin-web/src/views/tools/storage/index.vue create mode 100644 eladmin-web/src/views/tools/storage/local/index.vue create mode 100644 eladmin-web/src/views/tools/storage/qiniu/form.vue create mode 100644 eladmin-web/src/views/tools/storage/qiniu/index.vue create mode 100644 eladmin-web/src/views/tools/swagger/index.vue create mode 100644 eladmin-web/vue.config.js create mode 100644 pom.xml create mode 100644 sql/eladmin.sql create mode 100644 sql/finance.sql create mode 100644 sql/脚本如何选择.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f9d49d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +### IDEA ### +.idea/* +*.iml +*/target/* +*/*.iml +/.gradle/ +/application.pid +/.jpb/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9f53f73 --- /dev/null +++ b/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "{}" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright 2019-2023 Zheng Jie + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4b58280 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +先运行sql/finance.sql diff --git a/eladmin-common/pom.xml b/eladmin-common/pom.xml new file mode 100644 index 0000000..23d1024 --- /dev/null +++ b/eladmin-common/pom.xml @@ -0,0 +1,26 @@ + + + + eladmin + me.zhengjie + 2.7 + + 4.0.0 + + 5.8.20 + + + eladmin-common + 公共模块 + + + + + cn.hutool + hutool-all + ${hutool.version} + + + \ No newline at end of file diff --git a/eladmin-common/src/main/java/me/zhengjie/annotation/AnonymousAccess.java b/eladmin-common/src/main/java/me/zhengjie/annotation/AnonymousAccess.java new file mode 100644 index 0000000..b2c168f --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/annotation/AnonymousAccess.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.annotation; + +import java.lang.annotation.*; + +/** + * @author jacky + * 用于标记匿名访问方法 + */ +@Inherited +@Documented +@Target({ElementType.METHOD,ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface AnonymousAccess { + +} diff --git a/eladmin-common/src/main/java/me/zhengjie/annotation/DataPermission.java b/eladmin-common/src/main/java/me/zhengjie/annotation/DataPermission.java new file mode 100644 index 0000000..044d551 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/annotation/DataPermission.java @@ -0,0 +1,47 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + *

+ * 用于判断是否过滤数据权限 + * 1、如果没有用到 @OneToOne 这种关联关系,只需要填写 fieldName [参考:DeptQueryCriteria.class] + * 2、如果用到了 @OneToOne ,fieldName 和 joinName 都需要填写,拿UserQueryCriteria.class举例: + * 应该是 @DataPermission(joinName = "dept", fieldName = "id") + *

+ * @author Zheng Jie + * @website https://eladmin.vip + * @date 2020-05-07 + **/ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface DataPermission { + + /** + * Entity 中的字段名称 + */ + String fieldName() default ""; + + /** + * Entity 中与部门关联的字段名称 + */ + String joinName() default ""; +} diff --git a/eladmin-common/src/main/java/me/zhengjie/annotation/Limit.java b/eladmin-common/src/main/java/me/zhengjie/annotation/Limit.java new file mode 100644 index 0000000..58b41cf --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/annotation/Limit.java @@ -0,0 +1,49 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.annotation; + +import me.zhengjie.aspect.LimitType; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author jacky + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Limit { + + // 资源名称,用于描述接口功能 + String name() default ""; + + // 资源 key + String key() default ""; + + // key prefix + String prefix() default ""; + + // 时间的,单位秒 + int period(); + + // 限制访问次数 + int count(); + + // 限制类型 + LimitType limitType() default LimitType.CUSTOMER; + +} diff --git a/eladmin-common/src/main/java/me/zhengjie/annotation/Query.java b/eladmin-common/src/main/java/me/zhengjie/annotation/Query.java new file mode 100644 index 0000000..fe616f1 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/annotation/Query.java @@ -0,0 +1,92 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Zheng Jie + * @date 2019-6-4 13:52:30 + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Query { + + // Dong ZhaoYang 2017/8/7 基本对象的属性名 + String propName() default ""; + // Dong ZhaoYang 2017/8/7 查询方式 + Type type() default Type.EQUAL; + + /** + * 连接查询的属性名,如User类中的dept + */ + String joinName() default ""; + + /** + * 默认左连接 + */ + Join join() default Join.LEFT; + + /** + * 多字段模糊搜索,仅支持String类型字段,多个用逗号隔开, 如@Query(blurry = "email,username") + */ + String blurry() default ""; + + enum Type { + // jie 2019/6/4 相等 + EQUAL + // Dong ZhaoYang 2017/8/7 大于等于 + , GREATER_THAN + // Dong ZhaoYang 2017/8/7 小于等于 + , LESS_THAN + // Dong ZhaoYang 2017/8/7 中模糊查询 + , INNER_LIKE + // Dong ZhaoYang 2017/8/7 左模糊查询 + , LEFT_LIKE + // Dong ZhaoYang 2017/8/7 右模糊查询 + , RIGHT_LIKE + // Dong ZhaoYang 2017/8/7 小于 + , LESS_THAN_NQ + // jie 2019/6/4 包含 + , IN + // 不包含 + , NOT_IN + // 不等于 + ,NOT_EQUAL + // between + ,BETWEEN + // 不为空 + ,NOT_NULL + // 为空 + ,IS_NULL, + // Aborn Jiang 2022/06/01, 对应SQL: SELECT * FROM table WHERE FIND_IN_SET('querytag', table.tags); + FIND_IN_SET + } + + /** + * @author Zheng Jie + * 适用于简单连接查询,复杂的请自定义该注解,或者使用sql查询 + */ + enum Join { + /** jie 2019-6-4 13:18:30 */ + LEFT, RIGHT, INNER + } + +} + diff --git a/eladmin-common/src/main/java/me/zhengjie/annotation/rest/AnonymousDeleteMapping.java b/eladmin-common/src/main/java/me/zhengjie/annotation/rest/AnonymousDeleteMapping.java new file mode 100644 index 0000000..6a81c2e --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/annotation/rest/AnonymousDeleteMapping.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.zhengjie.annotation.rest; + +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 me.zhengjie.annotation.AnonymousAccess; +import org.springframework.core.annotation.AliasFor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +/** + * Annotation for mapping HTTP {@code DELETE} requests onto specific handler + * methods. + * 支持匿名访问 DeleteMapping + * + * @author liaojinlong + * @see AnonymousGetMapping + * @see AnonymousPostMapping + * @see AnonymousPutMapping + * @see AnonymousPatchMapping + * @see RequestMapping + */ +@AnonymousAccess +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RequestMapping(method = RequestMethod.DELETE) +public @interface AnonymousDeleteMapping { + + /** + * Alias for {@link RequestMapping#name}. + */ + @AliasFor(annotation = RequestMapping.class) + String name() default ""; + + /** + * Alias for {@link RequestMapping#value}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] value() default {}; + + /** + * Alias for {@link RequestMapping#path}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] path() default {}; + + /** + * Alias for {@link RequestMapping#params}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] params() default {}; + + /** + * Alias for {@link RequestMapping#headers}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] headers() default {}; + + /** + * Alias for {@link RequestMapping#consumes}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] consumes() default {}; + + /** + * Alias for {@link RequestMapping#produces}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] produces() default {}; + +} diff --git a/eladmin-common/src/main/java/me/zhengjie/annotation/rest/AnonymousGetMapping.java b/eladmin-common/src/main/java/me/zhengjie/annotation/rest/AnonymousGetMapping.java new file mode 100644 index 0000000..c260a71 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/annotation/rest/AnonymousGetMapping.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.zhengjie.annotation.rest; + +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 me.zhengjie.annotation.AnonymousAccess; +import org.springframework.core.annotation.AliasFor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +/** + * Annotation for mapping HTTP {@code GET} requests onto specific handler + * methods. + *

+ * 支持匿名访问 GetMapping + * + * @author liaojinlong + * @see RequestMapping + */ +@AnonymousAccess +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RequestMapping(method = RequestMethod.GET) +public @interface AnonymousGetMapping { + + /** + * Alias for {@link RequestMapping#name}. + */ + @AliasFor(annotation = RequestMapping.class) + String name() default ""; + + /** + * Alias for {@link RequestMapping#value}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] value() default {}; + + /** + * Alias for {@link RequestMapping#path}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] path() default {}; + + /** + * Alias for {@link RequestMapping#params}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] params() default {}; + + /** + * Alias for {@link RequestMapping#headers}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] headers() default {}; + + /** + * Alias for {@link RequestMapping#consumes}. + * + * @since 4.3.5 + */ + @AliasFor(annotation = RequestMapping.class) + String[] consumes() default {}; + + /** + * Alias for {@link RequestMapping#produces}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] produces() default {}; + +} diff --git a/eladmin-common/src/main/java/me/zhengjie/annotation/rest/AnonymousPatchMapping.java b/eladmin-common/src/main/java/me/zhengjie/annotation/rest/AnonymousPatchMapping.java new file mode 100644 index 0000000..6686617 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/annotation/rest/AnonymousPatchMapping.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.zhengjie.annotation.rest; + +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 me.zhengjie.annotation.AnonymousAccess; +import org.springframework.core.annotation.AliasFor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +/** + * Annotation for mapping HTTP {@code PATCH} requests onto specific handler + * methods. + * * 支持匿名访问 PatchMapping + * + * @author liaojinlong + * @see AnonymousGetMapping + * @see AnonymousPostMapping + * @see AnonymousPutMapping + * @see AnonymousDeleteMapping + * @see RequestMapping + */ +@AnonymousAccess +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RequestMapping(method = RequestMethod.PATCH) +public @interface AnonymousPatchMapping { + + /** + * Alias for {@link RequestMapping#name}. + */ + @AliasFor(annotation = RequestMapping.class) + String name() default ""; + + /** + * Alias for {@link RequestMapping#value}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] value() default {}; + + /** + * Alias for {@link RequestMapping#path}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] path() default {}; + + /** + * Alias for {@link RequestMapping#params}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] params() default {}; + + /** + * Alias for {@link RequestMapping#headers}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] headers() default {}; + + /** + * Alias for {@link RequestMapping#consumes}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] consumes() default {}; + + /** + * Alias for {@link RequestMapping#produces}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] produces() default {}; + +} diff --git a/eladmin-common/src/main/java/me/zhengjie/annotation/rest/AnonymousPostMapping.java b/eladmin-common/src/main/java/me/zhengjie/annotation/rest/AnonymousPostMapping.java new file mode 100644 index 0000000..8f1cdcd --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/annotation/rest/AnonymousPostMapping.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.zhengjie.annotation.rest; + +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 me.zhengjie.annotation.AnonymousAccess; +import org.springframework.core.annotation.AliasFor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +/** + * Annotation for mapping HTTP {@code POST} requests onto specific handler + * methods. + * 支持匿名访问 PostMapping + * + * @author liaojinlong + * @see AnonymousGetMapping + * @see AnonymousPostMapping + * @see AnonymousPutMapping + * @see AnonymousDeleteMapping + * @see RequestMapping + */ +@AnonymousAccess +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RequestMapping(method = RequestMethod.POST) +public @interface AnonymousPostMapping { + + /** + * Alias for {@link RequestMapping#name}. + */ + @AliasFor(annotation = RequestMapping.class) + String name() default ""; + + /** + * Alias for {@link RequestMapping#value}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] value() default {}; + + /** + * Alias for {@link RequestMapping#path}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] path() default {}; + + /** + * Alias for {@link RequestMapping#params}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] params() default {}; + + /** + * Alias for {@link RequestMapping#headers}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] headers() default {}; + + /** + * Alias for {@link RequestMapping#consumes}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] consumes() default {}; + + /** + * Alias for {@link RequestMapping#produces}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] produces() default {}; + +} diff --git a/eladmin-common/src/main/java/me/zhengjie/annotation/rest/AnonymousPutMapping.java b/eladmin-common/src/main/java/me/zhengjie/annotation/rest/AnonymousPutMapping.java new file mode 100644 index 0000000..7c417da --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/annotation/rest/AnonymousPutMapping.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.zhengjie.annotation.rest; + +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 me.zhengjie.annotation.AnonymousAccess; +import org.springframework.core.annotation.AliasFor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +/** + * Annotation for mapping HTTP {@code PUT} requests onto specific handler + * methods. + * * 支持匿名访问 PutMapping + * + * @author liaojinlong + * @see AnonymousGetMapping + * @see AnonymousPostMapping + * @see AnonymousPutMapping + * @see AnonymousDeleteMapping + * @see RequestMapping + */ +@AnonymousAccess +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RequestMapping(method = RequestMethod.PUT) +public @interface AnonymousPutMapping { + + /** + * Alias for {@link RequestMapping#name}. + */ + @AliasFor(annotation = RequestMapping.class) + String name() default ""; + + /** + * Alias for {@link RequestMapping#value}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] value() default {}; + + /** + * Alias for {@link RequestMapping#path}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] path() default {}; + + /** + * Alias for {@link RequestMapping#params}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] params() default {}; + + /** + * Alias for {@link RequestMapping#headers}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] headers() default {}; + + /** + * Alias for {@link RequestMapping#consumes}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] consumes() default {}; + + /** + * Alias for {@link RequestMapping#produces}. + */ + @AliasFor(annotation = RequestMapping.class) + String[] produces() default {}; + +} diff --git a/eladmin-common/src/main/java/me/zhengjie/aspect/LimitAspect.java b/eladmin-common/src/main/java/me/zhengjie/aspect/LimitAspect.java new file mode 100644 index 0000000..31f4040 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/aspect/LimitAspect.java @@ -0,0 +1,99 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.aspect; + +import com.google.common.collect.ImmutableList; +import me.zhengjie.annotation.Limit; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.utils.RequestHolder; +import me.zhengjie.utils.StringUtils; +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.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.stereotype.Component; +import javax.servlet.http.HttpServletRequest; +import java.lang.reflect.Method; + +/** + * @author / + */ +@Aspect +@Component +public class LimitAspect { + + private final RedisTemplate redisTemplate; + private static final Logger logger = LoggerFactory.getLogger(LimitAspect.class); + + public LimitAspect(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + @Pointcut("@annotation(me.zhengjie.annotation.Limit)") + public void pointcut() { + } + + @Around("pointcut()") + public Object around(ProceedingJoinPoint joinPoint) throws Throwable { + HttpServletRequest request = RequestHolder.getHttpServletRequest(); + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method signatureMethod = signature.getMethod(); + Limit limit = signatureMethod.getAnnotation(Limit.class); + LimitType limitType = limit.limitType(); + String key = limit.key(); + if (StringUtils.isEmpty(key)) { + if (limitType == LimitType.IP) { + key = StringUtils.getIp(request); + } else { + key = signatureMethod.getName(); + } + } + + ImmutableList keys = ImmutableList.of(StringUtils.join(limit.prefix(), "_", key, "_", request.getRequestURI().replace("/","_"))); + + String luaScript = buildLuaScript(); + RedisScript redisScript = new DefaultRedisScript<>(luaScript, Number.class); + Number count = redisTemplate.execute(redisScript, keys, limit.count(), limit.period()); + if (null != count && count.intValue() <= limit.count()) { + logger.info("第{}次访问key为 {},描述为 [{}] 的接口", count, keys, limit.name()); + return joinPoint.proceed(); + } else { + throw new BadRequestException("访问次数受限制"); + } + } + + /** + * 限流脚本 + */ + private String buildLuaScript() { + return "local c" + + "\nc = redis.call('get',KEYS[1])" + + "\nif c and tonumber(c) > tonumber(ARGV[1]) then" + + "\nreturn c;" + + "\nend" + + "\nc = redis.call('incr',KEYS[1])" + + "\nif tonumber(c) == 1 then" + + "\nredis.call('expire',KEYS[1],ARGV[2])" + + "\nend" + + "\nreturn c;"; + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/aspect/LimitType.java b/eladmin-common/src/main/java/me/zhengjie/aspect/LimitType.java new file mode 100644 index 0000000..bf3f09e --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/aspect/LimitType.java @@ -0,0 +1,27 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.aspect; + +/** + * 限流枚举 + * @author / + */ +public enum LimitType { + // 默认 + CUSTOMER, + // by ip addr + IP +} diff --git a/eladmin-common/src/main/java/me/zhengjie/base/BaseDTO.java b/eladmin-common/src/main/java/me/zhengjie/base/BaseDTO.java new file mode 100644 index 0000000..e9e7cc2 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/base/BaseDTO.java @@ -0,0 +1,40 @@ +package me.zhengjie.base; + +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.builder.ToStringBuilder; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.sql.Timestamp; + +/** + * @author Zheng Jie + * @date 2019年10月24日20:48:53 + */ +@Getter +@Setter +public class BaseDTO implements Serializable { + + private String createBy; + + private String updateBy; + + private Timestamp createTime; + + private Timestamp updateTime; + + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this); + Field[] fields = this.getClass().getDeclaredFields(); + try { + for (Field f : fields) { + f.setAccessible(true); + builder.append(f.getName(), f.get(this)).append("\n"); + } + } catch (Exception e) { + builder.append("toString builder encounter an error"); + } + return builder.toString(); + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/base/BaseEntity.java b/eladmin-common/src/main/java/me/zhengjie/base/BaseEntity.java new file mode 100644 index 0000000..618e100 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/base/BaseEntity.java @@ -0,0 +1,85 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.base; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.sql.Timestamp; + +/** + * 通用字段, is_del 根据需求自行添加 + * @author Zheng Jie + * @Date 2019年10月24日20:46:32 + */ +@Getter +@Setter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public class BaseEntity implements Serializable { + + @CreatedBy + @Column(name = "create_by", updatable = false) + @ApiModelProperty(value = "创建人", hidden = true) + private String createBy; + + @LastModifiedBy + @Column(name = "update_by") + @ApiModelProperty(value = "更新人", hidden = true) + private String updateBy; + + @CreationTimestamp + @Column(name = "create_time", updatable = false) + @ApiModelProperty(value = "创建时间", hidden = true) + private Timestamp createTime; + + @UpdateTimestamp + @Column(name = "update_time") + @ApiModelProperty(value = "更新时间", hidden = true) + private Timestamp updateTime; + + /* 分组校验 */ + public @interface Create {} + + /* 分组校验 */ + public @interface Update {} + + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this); + Field[] fields = this.getClass().getDeclaredFields(); + try { + for (Field f : fields) { + f.setAccessible(true); + builder.append(f.getName(), f.get(this)).append("\n"); + } + } catch (Exception e) { + builder.append("toString builder encounter an error"); + } + return builder.toString(); + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/base/BaseMapper.java b/eladmin-common/src/main/java/me/zhengjie/base/BaseMapper.java new file mode 100644 index 0000000..e8bb825 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/base/BaseMapper.java @@ -0,0 +1,53 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.base; + +import java.util.List; + +/** + * @author Zheng Jie + * @date 2018-11-23 + */ +public interface BaseMapper { + + /** + * DTO转Entity + * @param dto / + * @return / + */ + E toEntity(D dto); + + /** + * Entity转DTO + * @param entity / + * @return / + */ + D toDto(E entity); + + /** + * DTO集合转Entity集合 + * @param dtoList / + * @return / + */ + List toEntity(List dtoList); + + /** + * Entity集合转DTO集合 + * @param entityList / + * @return / + */ + List toDto(List entityList); +} diff --git a/eladmin-common/src/main/java/me/zhengjie/config/AuditorConfig.java b/eladmin-common/src/main/java/me/zhengjie/config/AuditorConfig.java new file mode 100644 index 0000000..006ef6b --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/config/AuditorConfig.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.config; + +import me.zhengjie.utils.SecurityUtils; +import org.springframework.data.domain.AuditorAware; +import org.springframework.stereotype.Component; +import java.util.Optional; + +/** + * @description : 设置审计 + * @author : Dong ZhaoYang + * @date : 2019/10/28 + */ +@Component("auditorAware") +public class AuditorConfig implements AuditorAware { + + /** + * 返回操作员标志信息 + * + * @return / + */ + @Override + public Optional getCurrentAuditor() { + try { + // 这里应根据实际业务情况获取具体信息 + return Optional.of(SecurityUtils.getCurrentUsername()); + }catch (Exception ignored){} + // 用户定时任务,或者无Token调用的情况 + return Optional.of("System"); + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/config/AuthorityConfig.java b/eladmin-common/src/main/java/me/zhengjie/config/AuthorityConfig.java new file mode 100644 index 0000000..658e621 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/config/AuthorityConfig.java @@ -0,0 +1,37 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.config; + +import me.zhengjie.utils.SecurityUtils; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Service; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Zheng Jie + */ +@Service(value = "el") +public class AuthorityConfig { + + public Boolean check(String ...permissions){ + // 获取当前用户的所有权限 + List elPermissions = SecurityUtils.getCurrentUser().getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()); + // 判断当前用户的所有权限是否包含接口上定义的权限 + return elPermissions.contains("admin") || Arrays.stream(permissions).anyMatch(elPermissions::contains); + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/config/FileProperties.java b/eladmin-common/src/main/java/me/zhengjie/config/FileProperties.java new file mode 100644 index 0000000..3d0a5c6 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/config/FileProperties.java @@ -0,0 +1,60 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.config; + +import lombok.Data; +import me.zhengjie.utils.ElConstant; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * @author Zheng Jie + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "file") +public class FileProperties { + + /** 文件大小限制 */ + private Long maxSize; + + /** 头像大小限制 */ + private Long avatarMaxSize; + + private ElPath mac; + + private ElPath linux; + + private ElPath windows; + + public ElPath getPath(){ + String os = System.getProperty("os.name"); + if(os.toLowerCase().startsWith(ElConstant.WIN)) { + return windows; + } else if(os.toLowerCase().startsWith(ElConstant.MAC)){ + return mac; + } + return linux; + } + + @Data + public static class ElPath{ + + private String path; + + private String avatar; + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/config/RedisConfig.java b/eladmin-common/src/main/java/me/zhengjie/config/RedisConfig.java new file mode 100644 index 0000000..d36a0be --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/config/RedisConfig.java @@ -0,0 +1,225 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.config; + +import cn.hutool.core.lang.Assert; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.parser.ParserConfig; +import com.alibaba.fastjson.serializer.SerializerFeature; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.digest.DigestUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.Cache; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.RedisSerializer; +import reactor.util.annotation.Nullable; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + + +/** + * @author Zheng Jie + * @date 2018-11-24 + */ +@Slf4j +@Configuration +@EnableCaching +@ConditionalOnClass(RedisOperations.class) +@EnableConfigurationProperties(RedisProperties.class) +public class RedisConfig extends CachingConfigurerSupport { + + /** + * 设置 redis 数据默认过期时间,默认2小时 + * 设置@cacheable 序列化方式 + */ + @Bean + public RedisCacheConfiguration redisCacheConfiguration(){ + FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class); + RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig(); + configuration = configuration.serializeValuesWith(RedisSerializationContext. + SerializationPair.fromSerializer(fastJsonRedisSerializer)).entryTtl(Duration.ofHours(2)); + return configuration; + } + + @SuppressWarnings("all") + @Bean(name = "redisTemplate") + @ConditionalOnMissingBean(name = "redisTemplate") + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + //序列化 + FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class); + // value值的序列化采用fastJsonRedisSerializer + template.setValueSerializer(fastJsonRedisSerializer); + template.setHashValueSerializer(fastJsonRedisSerializer); + // fastjson 升级到 1.2.83 后需要指定序列化白名单 + ParserConfig.getGlobalInstance().addAccept("me.zhengjie.domain"); + ParserConfig.getGlobalInstance().addAccept("me.zhengjie.service.dto"); + // 模块内的实体类 + ParserConfig.getGlobalInstance().addAccept("me.zhengjie.modules.mnt.domain"); + ParserConfig.getGlobalInstance().addAccept("me.zhengjie.modules.quartz.domain"); + ParserConfig.getGlobalInstance().addAccept("me.zhengjie.modules.system.domain"); + // 模块内的 Dto + ParserConfig.getGlobalInstance().addAccept("me.zhengjie.modules.mnt.service.dto"); + ParserConfig.getGlobalInstance().addAccept("me.zhengjie.modules.quartz.service.dto"); + ParserConfig.getGlobalInstance().addAccept("me.zhengjie.modules.security.service.dto"); + ParserConfig.getGlobalInstance().addAccept("me.zhengjie.modules.system.service.dto"); + // key的序列化采用StringRedisSerializer + template.setKeySerializer(new StringRedisSerializer()); + template.setHashKeySerializer(new StringRedisSerializer()); + template.setConnectionFactory(redisConnectionFactory); + return template; + } + + /** + * 自定义缓存key生成策略,默认将使用该策略 + */ + @Bean + @Override + public KeyGenerator keyGenerator() { + return (target, method, params) -> { + Map container = new HashMap<>(8); + Class targetClassClass = target.getClass(); + // 类地址 + container.put("class",targetClassClass.toGenericString()); + // 方法名称 + container.put("methodName",method.getName()); + // 包名称 + container.put("package",targetClassClass.getPackage()); + // 参数列表 + for (int i = 0; i < params.length; i++) { + container.put(String.valueOf(i),params[i]); + } + // 转为JSON字符串 + String jsonString = JSON.toJSONString(container); + // 做SHA256 Hash计算,得到一个SHA256摘要作为Key + return DigestUtils.sha256Hex(jsonString); + }; + } + + @Bean + @Override + @SuppressWarnings({"all"}) + public CacheErrorHandler errorHandler() { + // 异常处理,当Redis发生异常时,打印日志,但是程序正常走 + log.info("初始化 -> [{}]", "Redis CacheErrorHandler"); + return new CacheErrorHandler() { + @Override + public void handleCacheGetError(RuntimeException e, Cache cache, Object key) { + log.error("Redis occur handleCacheGetError:key -> [{}]", key, e); + } + + @Override + public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) { + log.error("Redis occur handleCachePutError:key -> [{}];value -> [{}]", key, value, e); + } + + @Override + public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) { + log.error("Redis occur handleCacheEvictError:key -> [{}]", key, e); + } + + @Override + public void handleCacheClearError(RuntimeException e, Cache cache) { + log.error("Redis occur handleCacheClearError:", e); + } + }; + } +} + +/** + * Value 序列化 + * + * @author / + * @param + */ +class FastJsonRedisSerializer implements RedisSerializer { + + private final Class clazz; + + FastJsonRedisSerializer(Class clazz) { + super(); + this.clazz = clazz; + } + + @Override + public byte[] serialize(T t) { + if (t == null) { + return new byte[0]; + } + return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(StandardCharsets.UTF_8); + } + + @Override + public T deserialize(byte[] bytes) { + if (bytes == null || bytes.length == 0) { + return null; + } + String str = new String(bytes, StandardCharsets.UTF_8); + return JSON.parseObject(str, clazz); + } + +} + +/** + * 重写序列化器 + * + * @author / + */ +class StringRedisSerializer implements RedisSerializer { + + private final Charset charset; + + StringRedisSerializer() { + this(StandardCharsets.UTF_8); + } + + private StringRedisSerializer(Charset charset) { + Assert.notNull(charset, "Charset must not be null!"); + this.charset = charset; + } + + @Override + public String deserialize(byte[] bytes) { + return (bytes == null ? null : new String(bytes, charset)); + } + + @Override + public @Nullable byte[] serialize(Object object) { + String string = JSON.toJSONString(object); + + if (org.apache.commons.lang3.StringUtils.isBlank(string)) { + return null; + } + string = string.replace("\"", ""); + return string.getBytes(charset); + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/config/RsaProperties.java b/eladmin-common/src/main/java/me/zhengjie/config/RsaProperties.java new file mode 100644 index 0000000..0a4a537 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/config/RsaProperties.java @@ -0,0 +1,38 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.config; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * @author Zheng Jie + * @website https://eladmin.vip + * @description + * @date 2020-05-18 + **/ +@Data +@Component +public class RsaProperties { + + public static String privateKey; + + @Value("${rsa.private_key}") + public void setPrivateKey(String privateKey) { + RsaProperties.privateKey = privateKey; + } +} \ No newline at end of file diff --git a/eladmin-common/src/main/java/me/zhengjie/config/SwaggerConfig.java b/eladmin-common/src/main/java/me/zhengjie/config/SwaggerConfig.java new file mode 100644 index 0000000..1edbcc4 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/config/SwaggerConfig.java @@ -0,0 +1,151 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.config; + +import cn.hutool.core.collection.CollUtil; +import com.fasterxml.classmate.TypeResolver; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.data.domain.Pageable; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.schema.AlternateTypeRule; +import springfox.documentation.schema.AlternateTypeRuleConvention; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.ApiKey; +import springfox.documentation.service.AuthorizationScope; +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; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.util.ArrayList; +import java.util.List; +import static springfox.documentation.schema.AlternateTypeRules.newRule; + +/** + * api页面 /doc.html + * @author Zheng Jie + * @date 2018-11-23 + */ +@Configuration +@EnableSwagger2 +public class SwaggerConfig { + + @Value("${jwt.header}") + private String tokenHeader; + + @Value("${swagger.enabled}") + private Boolean enabled; + + @Bean + @SuppressWarnings("all") + public Docket createRestApi() { + return new Docket(DocumentationType.SWAGGER_2) + .enable(enabled) + .pathMapping("/") + .apiInfo(apiInfo()) + .select() + .paths(PathSelectors.regex("^(?!/error).*")) + .paths(PathSelectors.any()) + .build() + //添加登陆认证 + .securitySchemes(securitySchemes()) + .securityContexts(securityContexts()); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .description("一个简单且易上手的 Spring boot 后台管理框架") + .title("ELADMIN 接口文档") + .version("2.7") + .build(); + } + + private List securitySchemes() { + //设置请求头信息 + List securitySchemes = new ArrayList<>(); + ApiKey apiKey = new ApiKey(tokenHeader, tokenHeader, "header"); + securitySchemes.add(apiKey); + return securitySchemes; + } + + private List securityContexts() { + //设置需要登录认证的路径 + List securityContexts = new ArrayList<>(); + securityContexts.add(getContextByPath()); + return securityContexts; + } + + private SecurityContext getContextByPath() { + return SecurityContext.builder() + .securityReferences(defaultAuth()) + // 表示 /auth/code、/auth/login 接口不需要使用securitySchemes即不需要带token + .operationSelector(o->o.requestMappingPattern().matches("^(?!/auth/code|/auth/login).*$")) + .build(); + } + + private List defaultAuth() { + List securityReferences = new ArrayList<>(); + AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything"); + AuthorizationScope[] authorizationScopes = new AuthorizationScope[1]; + authorizationScopes[0] = authorizationScope; + securityReferences.add(new SecurityReference(tokenHeader, authorizationScopes)); + return securityReferences; + } +} + +/** + * 将Pageable转换展示在swagger中 + */ +@Configuration +class SwaggerDataConfig { + + @Bean + public AlternateTypeRuleConvention pageableConvention(final TypeResolver resolver) { + return new AlternateTypeRuleConvention() { + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; + } + + @Override + public List rules() { + return CollUtil.newArrayList(newRule(resolver.resolve(Pageable.class), resolver.resolve(Page.class))); + } + }; + } + + @ApiModel + @Data + private static class Page { + @ApiModelProperty("页码 (0..N)") + private Integer page; + + @ApiModelProperty("每页显示的数目") + private Integer size; + + @ApiModelProperty("以下列格式排序标准:property[,asc | desc]。 默认排序顺序为升序。 支持多种排序条件:如:id,asc") + private List sort; + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/exception/BadConfigurationException.java b/eladmin-common/src/main/java/me/zhengjie/exception/BadConfigurationException.java new file mode 100644 index 0000000..ede3691 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/exception/BadConfigurationException.java @@ -0,0 +1,98 @@ +/* + * Copyright 2019-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.exception; + +/** + * 统一关于错误配置信息 异常 + * + * @author: liaojinlong + * @date: 2020/6/10 18:06 + */ +public class BadConfigurationException extends RuntimeException { + /** + * Constructs a new runtime exception with {@code null} as its + * detail message. The cause is not initialized, and may subsequently be + * initialized by a call to {@link #initCause}. + */ + public BadConfigurationException() { + super(); + } + + /** + * Constructs a new runtime exception with the specified detail message. + * The cause is not initialized, and may subsequently be initialized by a + * call to {@link #initCause}. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public BadConfigurationException(String message) { + super(message); + } + + /** + * Constructs a new runtime exception with the specified detail message and + * cause.

Note that the detail message associated with + * {@code cause} is not automatically incorporated in + * this runtime exception's detail message. + * + * @param message the detail message (which is saved for later retrieval + * by the {@link #getMessage()} method). + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A {@code null} value is + * permitted, and indicates that the cause is nonexistent or + * unknown.) + * @since 1.4 + */ + public BadConfigurationException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new runtime exception with the specified cause and a + * detail message of {@code (cause==null ? null : cause.toString())} + * (which typically contains the class and detail message of + * {@code cause}). This constructor is useful for runtime exceptions + * that are little more than wrappers for other throwables. + * + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A {@code null} value is + * permitted, and indicates that the cause is nonexistent or + * unknown.) + * @since 1.4 + */ + public BadConfigurationException(Throwable cause) { + super(cause); + } + + /** + * Constructs a new runtime exception with the specified detail + * message, cause, suppression enabled or disabled, and writable + * stack trace enabled or disabled. + * + * @param message the detail message. + * @param cause the cause. (A {@code null} value is permitted, + * and indicates that the cause is nonexistent or unknown.) + * @param enableSuppression whether or not suppression is enabled + * or disabled + * @param writableStackTrace whether or not the stack trace should + * be writable + * @since 1.7 + */ + protected BadConfigurationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/exception/BadRequestException.java b/eladmin-common/src/main/java/me/zhengjie/exception/BadRequestException.java new file mode 100644 index 0000000..900453b --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/exception/BadRequestException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +/** + * @author Zheng Jie + * @date 2018-11-23 + * 统一异常处理 + */ +@Getter +public class BadRequestException extends RuntimeException{ + + private Integer status = BAD_REQUEST.value(); + + public BadRequestException(String msg){ + super(msg); + } + + public BadRequestException(HttpStatus status,String msg){ + super(msg); + this.status = status.value(); + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/exception/EntityExistException.java b/eladmin-common/src/main/java/me/zhengjie/exception/EntityExistException.java new file mode 100644 index 0000000..03f9bf2 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/exception/EntityExistException.java @@ -0,0 +1,34 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.exception; + +import org.springframework.util.StringUtils; + +/** + * @author Zheng Jie + * @date 2018-11-23 + */ +public class EntityExistException extends RuntimeException { + + public EntityExistException(Class clazz, String field, String val) { + super(EntityExistException.generateMessage(clazz.getSimpleName(), field, val)); + } + + private static String generateMessage(String entity, String field, String val) { + return StringUtils.capitalize(entity) + + " with " + field + " "+ val + " existed"; + } +} \ No newline at end of file diff --git a/eladmin-common/src/main/java/me/zhengjie/exception/EntityNotFoundException.java b/eladmin-common/src/main/java/me/zhengjie/exception/EntityNotFoundException.java new file mode 100644 index 0000000..bcdc956 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/exception/EntityNotFoundException.java @@ -0,0 +1,34 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.exception; + +import org.springframework.util.StringUtils; + +/** + * @author Zheng Jie + * @date 2018-11-23 + */ +public class EntityNotFoundException extends RuntimeException { + + public EntityNotFoundException(Class clazz, String field, String val) { + super(EntityNotFoundException.generateMessage(clazz.getSimpleName(), field, val)); + } + + private static String generateMessage(String entity, String field, String val) { + return StringUtils.capitalize(entity) + + " with " + field + " "+ val + " does not exist"; + } +} \ No newline at end of file diff --git a/eladmin-common/src/main/java/me/zhengjie/exception/handler/ApiError.java b/eladmin-common/src/main/java/me/zhengjie/exception/handler/ApiError.java new file mode 100644 index 0000000..977788c --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/exception/handler/ApiError.java @@ -0,0 +1,49 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.exception.handler; + +import lombok.Data; + +/** + * @author Zheng Jie + * @date 2018-11-23 + */ +@Data +class ApiError { + + private Integer status = 400; + private Long timestamp; + private String message; + + private ApiError() { + timestamp = System.currentTimeMillis(); + } + + public static ApiError error(String message){ + ApiError apiError = new ApiError(); + apiError.setMessage(message); + return apiError; + } + + public static ApiError error(Integer status, String message){ + ApiError apiError = new ApiError(); + apiError.setStatus(status); + apiError.setMessage(message); + return apiError; + } +} + + diff --git a/eladmin-common/src/main/java/me/zhengjie/exception/handler/GlobalExceptionHandler.java b/eladmin-common/src/main/java/me/zhengjie/exception/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..23cbb38 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/exception/handler/GlobalExceptionHandler.java @@ -0,0 +1,113 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.exception.handler; + +import lombok.extern.slf4j.Slf4j; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.exception.EntityExistException; +import me.zhengjie.exception.EntityNotFoundException; +import me.zhengjie.utils.ThrowableUtil; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import static org.springframework.http.HttpStatus.*; + +/** + * @author Zheng Jie + * @date 2018-11-23 + */ +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * 处理所有不可知的异常 + */ + @ExceptionHandler(Throwable.class) + public ResponseEntity handleException(Throwable e){ + // 打印堆栈信息 + log.error(ThrowableUtil.getStackTrace(e)); + return buildResponseEntity(ApiError.error(e.getMessage())); + } + + /** + * BadCredentialsException + */ + @ExceptionHandler(BadCredentialsException.class) + public ResponseEntity badCredentialsException(BadCredentialsException e){ + // 打印堆栈信息 + String message = "坏的凭证".equals(e.getMessage()) ? "用户名或密码不正确" : e.getMessage(); + log.error(message); + return buildResponseEntity(ApiError.error(message)); + } + + /** + * 处理自定义异常 + */ + @ExceptionHandler(value = BadRequestException.class) + public ResponseEntity badRequestException(BadRequestException e) { + // 打印堆栈信息 + log.error(ThrowableUtil.getStackTrace(e)); + return buildResponseEntity(ApiError.error(e.getStatus(),e.getMessage())); + } + + /** + * 处理 EntityExist + */ + @ExceptionHandler(value = EntityExistException.class) + public ResponseEntity entityExistException(EntityExistException e) { + // 打印堆栈信息 + log.error(ThrowableUtil.getStackTrace(e)); + return buildResponseEntity(ApiError.error(e.getMessage())); + } + + /** + * 处理 EntityNotFound + */ + @ExceptionHandler(value = EntityNotFoundException.class) + public ResponseEntity entityNotFoundException(EntityNotFoundException e) { + // 打印堆栈信息 + log.error(ThrowableUtil.getStackTrace(e)); + return buildResponseEntity(ApiError.error(NOT_FOUND.value(),e.getMessage())); + } + + /** + * 处理所有接口数据验证异常 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e){ + // 打印堆栈信息 + log.error(ThrowableUtil.getStackTrace(e)); + ObjectError objectError = e.getBindingResult().getAllErrors().get(0); + String message = objectError.getDefaultMessage(); + if (objectError instanceof FieldError) { + message = ((FieldError) objectError).getField() + ": " + message; + } + return buildResponseEntity(ApiError.error(message)); + } + + /** + * 统一返回 + */ + private ResponseEntity buildResponseEntity(ApiError apiError) { + return new ResponseEntity<>(apiError, HttpStatus.valueOf(apiError.getStatus())); + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/CacheKey.java b/eladmin-common/src/main/java/me/zhengjie/utils/CacheKey.java new file mode 100644 index 0000000..7485713 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/utils/CacheKey.java @@ -0,0 +1,58 @@ +/* + * Copyright 2019-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.utils; + +/** + * @author: liaojinlong + * @date: 2020/6/11 15:49 + * @apiNote: 关于缓存的Key集合 + */ +public interface CacheKey { + + /** + * 用户 + */ + String USER_ID = "user::id:"; + /** + * 数据 + */ + String DATA_USER = "data::user:"; + /** + * 菜单 + */ + String MENU_ID = "menu::id:"; + String MENU_USER = "menu::user:"; + /** + * 角色授权 + */ + String ROLE_AUTH = "role::auth:"; + /** + * 角色信息 + */ + String ROLE_ID = "role::id:"; + /** + * 部门 + */ + String DEPT_ID = "dept::id:"; + /** + * 岗位 + */ + String JOB_ID = "job::id:"; + /** + * 数据字典 + */ + String DICT_NAME = "dict::name:"; +} diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/CallBack.java b/eladmin-common/src/main/java/me/zhengjie/utils/CallBack.java new file mode 100644 index 0000000..9b10812 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/utils/CallBack.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.zhengjie.utils; + +/** + * @author: liaojinlong + * @date: 2020/6/9 17:02 + * @since: 1.0 + * @see {@link SpringContextHolder} + * 针对某些初始化方法,在SpringContextHolder 初始化前时,
+ * 可提交一个 提交回调任务。
+ * 在SpringContextHolder 初始化后,进行回调使用 + */ + +public interface CallBack { + /** + * 回调执行方法 + */ + void executor(); + + /** + * 本回调任务名称 + * @return / + */ + default String getCallBackName() { + return Thread.currentThread().getId() + ":" + this.getClass().getName(); + } +} + diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/CloseUtil.java b/eladmin-common/src/main/java/me/zhengjie/utils/CloseUtil.java new file mode 100644 index 0000000..f39919f --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/utils/CloseUtil.java @@ -0,0 +1,47 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.utils; + +import java.io.Closeable; + +/** + * @author Zheng Jie + * @website https://eladmin.vip + * @description 用于关闭各种连接,缺啥补啥 + * @date 2021-03-05 + **/ +public class CloseUtil { + + public static void close(Closeable closeable) { + if (null != closeable) { + try { + closeable.close(); + } catch (Exception e) { + // 静默关闭 + } + } + } + + public static void close(AutoCloseable closeable) { + if (null != closeable) { + try { + closeable.close(); + } catch (Exception e) { + // 静默关闭 + } + } + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/DateUtil.java b/eladmin-common/src/main/java/me/zhengjie/utils/DateUtil.java new file mode 100644 index 0000000..0b0bf63 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/utils/DateUtil.java @@ -0,0 +1,160 @@ +/* + * Copyright 2019-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.zhengjie.utils; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.util.Date; + +/** + * @author: liaojinlong + * @date: 2020/6/11 16:28 + * @apiNote: JDK 8 新日期类 格式化与字符串转换 工具类 + */ +public class DateUtil { + + public static final DateTimeFormatter DFY_MD_HMS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + public static final DateTimeFormatter DFY_MD = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + /** + * LocalDateTime 转时间戳 + * + * @param localDateTime / + * @return / + */ + public static Long getTimeStamp(LocalDateTime localDateTime) { + return localDateTime.atZone(ZoneId.systemDefault()).toEpochSecond(); + } + + /** + * 时间戳转LocalDateTime + * + * @param timeStamp / + * @return / + */ + public static LocalDateTime fromTimeStamp(Long timeStamp) { + return LocalDateTime.ofEpochSecond(timeStamp, 0, OffsetDateTime.now().getOffset()); + } + + /** + * LocalDateTime 转 Date + * Jdk8 后 不推荐使用 {@link Date} Date + * + * @param localDateTime / + * @return / + */ + public static Date toDate(LocalDateTime localDateTime) { + return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()); + } + + /** + * LocalDate 转 Date + * Jdk8 后 不推荐使用 {@link Date} Date + * + * @param localDate / + * @return / + */ + public static Date toDate(LocalDate localDate) { + return toDate(localDate.atTime(LocalTime.now(ZoneId.systemDefault()))); + } + + + /** + * Date转 LocalDateTime + * Jdk8 后 不推荐使用 {@link Date} Date + * + * @param date / + * @return / + */ + public static LocalDateTime toLocalDateTime(Date date) { + return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()); + } + + /** + * 日期 格式化 + * + * @param localDateTime / + * @param patten / + * @return / + */ + public static String localDateTimeFormat(LocalDateTime localDateTime, String patten) { + DateTimeFormatter df = DateTimeFormatter.ofPattern(patten); + return df.format(localDateTime); + } + + /** + * 日期 格式化 + * + * @param localDateTime / + * @param df / + * @return / + */ + public static String localDateTimeFormat(LocalDateTime localDateTime, DateTimeFormatter df) { + return df.format(localDateTime); + } + + /** + * 日期格式化 yyyy-MM-dd HH:mm:ss + * + * @param localDateTime / + * @return / + */ + public static String localDateTimeFormatyMdHms(LocalDateTime localDateTime) { + return DFY_MD_HMS.format(localDateTime); + } + + /** + * 日期格式化 yyyy-MM-dd + * + * @param localDateTime / + * @return / + */ + public String localDateTimeFormatyMd(LocalDateTime localDateTime) { + return DFY_MD.format(localDateTime); + } + + /** + * 字符串转 LocalDateTime ,字符串格式 yyyy-MM-dd + * + * @param localDateTime / + * @return / + */ + public static LocalDateTime parseLocalDateTimeFormat(String localDateTime, String pattern) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern); + return LocalDateTime.from(dateTimeFormatter.parse(localDateTime)); + } + + /** + * 字符串转 LocalDateTime ,字符串格式 yyyy-MM-dd + * + * @param localDateTime / + * @return / + */ + public static LocalDateTime parseLocalDateTimeFormat(String localDateTime, DateTimeFormatter dateTimeFormatter) { + return LocalDateTime.from(dateTimeFormatter.parse(localDateTime)); + } + + /** + * 字符串转 LocalDateTime ,字符串格式 yyyy-MM-dd HH:mm:ss + * + * @param localDateTime / + * @return / + */ + public static LocalDateTime parseLocalDateTimeFormatyMdHms(String localDateTime) { + return LocalDateTime.from(DFY_MD_HMS.parse(localDateTime)); + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/ElConstant.java b/eladmin-common/src/main/java/me/zhengjie/utils/ElConstant.java new file mode 100644 index 0000000..479b1e6 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/utils/ElConstant.java @@ -0,0 +1,34 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.utils; + +/** + * 常用静态常量 + * + * @author Zheng Jie + * @date 2018-12-26 + */ +public class ElConstant { + /** + * win 系统 + */ + public static final String WIN = "win"; + + /** + * mac 系统 + */ + public static final String MAC = "mac"; +} diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/EncryptUtils.java b/eladmin-common/src/main/java/me/zhengjie/utils/EncryptUtils.java new file mode 100644 index 0000000..4f334aa --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/utils/EncryptUtils.java @@ -0,0 +1,100 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.utils; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; +import javax.crypto.spec.IvParameterSpec; +import java.nio.charset.StandardCharsets; + +/** + * 加密 + * @author Zheng Jie + * @date 2018-11-23 + */ + +public class EncryptUtils { + + private static final String STR_PARAM = "Passw0rd"; + + private static Cipher cipher; + + private static final IvParameterSpec IV = new IvParameterSpec(STR_PARAM.getBytes(StandardCharsets.UTF_8)); + + private static DESKeySpec getDesKeySpec(String source) throws Exception { + if (source == null || source.length() == 0){ + return null; + } + cipher = Cipher.getInstance("DES/CBC/PKCS5Padding"); + String strKey = "Passw0rd"; + return new DESKeySpec(strKey.getBytes(StandardCharsets.UTF_8)); + } + + /** + * 对称加密 + */ + public static String desEncrypt(String source) throws Exception { + DESKeySpec desKeySpec = getDesKeySpec(source); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); + SecretKey secretKey = keyFactory.generateSecret(desKeySpec); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, IV); + return byte2hex( + cipher.doFinal(source.getBytes(StandardCharsets.UTF_8))).toUpperCase(); + } + + /** + * 对称解密 + */ + public static String desDecrypt(String source) throws Exception { + byte[] src = hex2byte(source.getBytes(StandardCharsets.UTF_8)); + DESKeySpec desKeySpec = getDesKeySpec(source); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); + SecretKey secretKey = keyFactory.generateSecret(desKeySpec); + cipher.init(Cipher.DECRYPT_MODE, secretKey, IV); + byte[] retByte = cipher.doFinal(src); + return new String(retByte); + } + + private static String byte2hex(byte[] inStr) { + String stmp; + StringBuilder out = new StringBuilder(inStr.length * 2); + for (byte b : inStr) { + stmp = Integer.toHexString(b & 0xFF); + if (stmp.length() == 1) { + // 如果是0至F的单位字符串,则添加0 + out.append("0").append(stmp); + } else { + out.append(stmp); + } + } + return out.toString(); + } + + private static byte[] hex2byte(byte[] b) { + int size = 2; + if ((b.length % size) != 0){ + throw new IllegalArgumentException("长度不是偶数"); + } + byte[] b2 = new byte[b.length / 2]; + for (int n = 0; n < b.length; n += size) { + String item = new String(b, n, 2); + b2[n / 2] = (byte) Integer.parseInt(item, 16); + } + return b2; + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/FileUtil.java b/eladmin-common/src/main/java/me/zhengjie/utils/FileUtil.java new file mode 100644 index 0000000..ca2b674 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/utils/FileUtil.java @@ -0,0 +1,395 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.utils; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.poi.excel.BigExcelWriter; +import cn.hutool.poi.excel.ExcelUtil; +import me.zhengjie.exception.BadRequestException; +import org.apache.poi.util.IOUtils; +import org.apache.poi.xssf.streaming.SXSSFSheet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.multipart.MultipartFile; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.security.MessageDigest; +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * File工具类,扩展 hutool 工具包 + * + * @author Zheng Jie + * @date 2018-12-27 + */ +public class FileUtil extends cn.hutool.core.io.FileUtil { + + private static final Logger log = LoggerFactory.getLogger(FileUtil.class); + + /** + * 系统临时目录 + *
+ * windows 包含路径分割符,但Linux 不包含, + * 在windows \\==\ 前提下, + * 为安全起见 同意拼装 路径分割符, + *

+     *       java.io.tmpdir
+     *       windows : C:\Users/xxx\AppData\Local\Temp\
+     *       linux: /temp
+     * 
+ */ + public static final String SYS_TEM_DIR = System.getProperty("java.io.tmpdir") + File.separator; + /** + * 定义GB的计算常量 + */ + private static final int GB = 1024 * 1024 * 1024; + /** + * 定义MB的计算常量 + */ + private static final int MB = 1024 * 1024; + /** + * 定义KB的计算常量 + */ + private static final int KB = 1024; + + /** + * 格式化小数 + */ + private static final DecimalFormat DF = new DecimalFormat("0.00"); + + public static final String IMAGE = "图片"; + public static final String TXT = "文档"; + public static final String MUSIC = "音乐"; + public static final String VIDEO = "视频"; + public static final String OTHER = "其他"; + + + /** + * MultipartFile转File + */ + public static File toFile(MultipartFile multipartFile) { + // 获取文件名 + String fileName = multipartFile.getOriginalFilename(); + // 获取文件后缀 + String prefix = "." + getExtensionName(fileName); + File file = null; + try { + // 用uuid作为文件名,防止生成的临时文件重复 + file = new File(SYS_TEM_DIR + IdUtil.simpleUUID() + prefix); + // MultipartFile to File + multipartFile.transferTo(file); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + return file; + } + + /** + * 获取文件扩展名,不带 . + */ + public static String getExtensionName(String filename) { + if ((filename != null) && (filename.length() > 0)) { + int dot = filename.lastIndexOf('.'); + if ((dot > -1) && (dot < (filename.length() - 1))) { + return filename.substring(dot + 1); + } + } + return filename; + } + + /** + * Java文件操作 获取不带扩展名的文件名 + */ + public static String getFileNameNoEx(String filename) { + if ((filename != null) && (filename.length() > 0)) { + int dot = filename.lastIndexOf('.'); + if ((dot > -1) && (dot < (filename.length()))) { + return filename.substring(0, dot); + } + } + return filename; + } + + /** + * 文件大小转换 + */ + public static String getSize(long size) { + String resultSize; + if (size / GB >= 1) { + //如果当前Byte的值大于等于1GB + resultSize = DF.format(size / (float) GB) + "GB "; + } else if (size / MB >= 1) { + //如果当前Byte的值大于等于1MB + resultSize = DF.format(size / (float) MB) + "MB "; + } else if (size / KB >= 1) { + //如果当前Byte的值大于等于1KB + resultSize = DF.format(size / (float) KB) + "KB "; + } else { + resultSize = size + "B "; + } + return resultSize; + } + + /** + * inputStream 转 File + */ + static File inputStreamToFile(InputStream ins, String name){ + File file = new File(SYS_TEM_DIR + name); + if (file.exists()) { + return file; + } + OutputStream os = null; + try { + os = new FileOutputStream(file); + int bytesRead; + int len = 8192; + byte[] buffer = new byte[len]; + while ((bytesRead = ins.read(buffer, 0, len)) != -1) { + os.write(buffer, 0, bytesRead); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + CloseUtil.close(os); + CloseUtil.close(ins); + } + return file; + } + + /** + * 将文件名解析成文件的上传路径 + */ + public static File upload(MultipartFile file, String filePath) { + Date date = new Date(); + SimpleDateFormat format = new SimpleDateFormat("yyyyMMddhhmmssS"); + // 过滤非法文件名 + String name = getFileNameNoEx(verifyFilename(file.getOriginalFilename())); + String suffix = getExtensionName(file.getOriginalFilename()); + String nowStr = "-" + format.format(date); + try { + String fileName = name + nowStr + "." + suffix; + String path = filePath + fileName; + // getCanonicalFile 可解析正确各种路径 + File dest = new File(path).getCanonicalFile(); + // 检测是否存在目录 + if (!dest.getParentFile().exists()) { + if (!dest.getParentFile().mkdirs()) { + System.out.println("was not successful."); + } + } + // 文件写入 + file.transferTo(dest); + return dest; + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + /** + * 导出excel + */ + public static void downloadExcel(List> list, HttpServletResponse response) throws IOException { + String tempPath = SYS_TEM_DIR + IdUtil.fastSimpleUUID() + ".xlsx"; + File file = new File(tempPath); + BigExcelWriter writer = ExcelUtil.getBigWriter(file); + // 一次性写出内容,使用默认样式,强制输出标题 + writer.write(list, true); + SXSSFSheet sheet = (SXSSFSheet)writer.getSheet(); + //上面需要强转SXSSFSheet 不然没有trackAllColumnsForAutoSizing方法 + sheet.trackAllColumnsForAutoSizing(); + //列宽自适应 + writer.autoSizeColumnAll(); + //response为HttpServletResponse对象 + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"); + //test.xls是弹出下载对话框的文件名,不能为中文,中文请自行编码 + response.setHeader("Content-Disposition", "attachment;filename=file.xlsx"); + ServletOutputStream out = response.getOutputStream(); + // 终止后删除临时文件 + file.deleteOnExit(); + writer.flush(out, true); + //此处记得关闭输出Servlet流 + IoUtil.close(out); + } + + public static String getFileType(String type) { + String documents = "txt doc pdf ppt pps xlsx xls docx"; + String music = "mp3 wav wma mpa ram ra aac aif m4a"; + String video = "avi mpg mpe mpeg asf wmv mov qt rm mp4 flv m4v webm ogv ogg"; + String image = "bmp dib pcp dif wmf gif jpg tif eps psd cdr iff tga pcd mpt png jpeg"; + if (image.contains(type)) { + return IMAGE; + } else if (documents.contains(type)) { + return TXT; + } else if (music.contains(type)) { + return MUSIC; + } else if (video.contains(type)) { + return VIDEO; + } else { + return OTHER; + } + } + + public static void checkSize(long maxSize, long size) { + // 1M + int len = 1024 * 1024; + if (size > (maxSize * len)) { + throw new BadRequestException("文件超出规定大小:" + maxSize + "MB"); + } + } + + /** + * 判断两个文件是否相同 + */ + public static boolean check(File file1, File file2) { + String img1Md5 = getMd5(file1); + String img2Md5 = getMd5(file2); + if(img1Md5 != null){ + return img1Md5.equals(img2Md5); + } + return false; + } + + /** + * 判断两个文件是否相同 + */ + public static boolean check(String file1Md5, String file2Md5) { + return file1Md5.equals(file2Md5); + } + + private static byte[] getByte(File file) { + // 得到文件长度 + byte[] b = new byte[(int) file.length()]; + InputStream in = null; + try { + in = new FileInputStream(file); + try { + System.out.println(in.read(b)); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } finally { + CloseUtil.close(in); + } + return b; + } + + private static String getMd5(byte[] bytes) { + // 16进制字符 + char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + try { + MessageDigest mdTemp = MessageDigest.getInstance("MD5"); + mdTemp.update(bytes); + byte[] md = mdTemp.digest(); + int j = md.length; + char[] str = new char[j * 2]; + int k = 0; + // 移位 输出字符串 + for (byte byte0 : md) { + str[k++] = hexDigits[byte0 >>> 4 & 0xf]; + str[k++] = hexDigits[byte0 & 0xf]; + } + return new String(str); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + /** + * 下载文件 + * + * @param request / + * @param response / + * @param file / + */ + public static void downloadFile(HttpServletRequest request, HttpServletResponse response, File file, boolean deleteOnExit) { + response.setCharacterEncoding(request.getCharacterEncoding()); + response.setContentType("application/octet-stream"); + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + response.setHeader("Content-Disposition", "attachment; filename=" + file.getName()); + IOUtils.copy(fis, response.getOutputStream()); + response.flushBuffer(); + } catch (Exception e) { + log.error(e.getMessage(), e); + } finally { + if (fis != null) { + try { + fis.close(); + if (deleteOnExit) { + file.deleteOnExit(); + } + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + } + } + + /** + * 验证并过滤非法的文件名 + * @param fileName 文件名 + * @return 文件名 + */ + public static String verifyFilename(String fileName) { + // 过滤掉特殊字符 + fileName = fileName.replaceAll("[\\\\/:*?\"<>|~\\s]", ""); + + // 去掉文件名开头和结尾的空格和点 + fileName = fileName.trim().replaceAll("^[. ]+|[. ]+$", ""); + + // 不允许文件名超过255(在Mac和Linux中)或260(在Windows中)个字符 + int maxFileNameLength = 255; + if (System.getProperty("os.name").startsWith("Windows")) { + maxFileNameLength = 260; + } + if (fileName.length() > maxFileNameLength) { + fileName = fileName.substring(0, maxFileNameLength); + } + + // 过滤掉控制字符 + fileName = fileName.replaceAll("[\\p{Cntrl}]", ""); + + // 过滤掉 ".." 路径 + fileName = fileName.replaceAll("\\.{2,}", ""); + + // 去掉文件名开头的 ".." + fileName = fileName.replaceAll("^\\.+/", ""); + + // 保留文件名中最后一个 "." 字符,过滤掉其他 "." + fileName = fileName.replaceAll("^(.*)(\\.[^.]*)$", "$1").replaceAll("\\.", "") + + fileName.replaceAll("^(.*)(\\.[^.]*)$", "$2"); + + return fileName; + } + + + public static String getMd5(File file) { + return getMd5(getByte(file)); + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/PageResult.java b/eladmin-common/src/main/java/me/zhengjie/utils/PageResult.java new file mode 100644 index 0000000..0f1fdfe --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/utils/PageResult.java @@ -0,0 +1,16 @@ +package me.zhengjie.utils; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@Getter +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +public class PageResult { + + private final List content; + + private final long totalElements; +} diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/PageUtil.java b/eladmin-common/src/main/java/me/zhengjie/utils/PageUtil.java new file mode 100644 index 0000000..56020d9 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/utils/PageUtil.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.utils; + +import org.springframework.data.domain.Page; +import java.util.*; + +/** + * 分页工具 + * @author Zheng Jie + * @date 2018-12-10 + */ +public class PageUtil extends cn.hutool.core.util.PageUtil { + + /** + * List 分页 + */ + public static List paging(int page, int size , List list) { + int fromIndex = page * size; + int toIndex = page * size + size; + if(fromIndex > list.size()){ + return Collections.emptyList(); + } else if(toIndex >= list.size()) { + return list.subList(fromIndex,list.size()); + } else { + return list.subList(fromIndex,toIndex); + } + } + + /** + * Page 数据处理,预防redis反序列化报错 + */ + public static PageResult toPage(Page page) { + return new PageResult<>(page.getContent(), page.getTotalElements()); + } + + /** + * 自定义分页 + */ + public static PageResult toPage(List list, long totalElements) { + return new PageResult<>(list, totalElements); + } + + /** + * 返回空数据 + */ + public static PageResult noData () { + return new PageResult<>(null, 0); + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/QueryHelp.java b/eladmin-common/src/main/java/me/zhengjie/utils/QueryHelp.java new file mode 100644 index 0000000..587ece9 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/utils/QueryHelp.java @@ -0,0 +1,218 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import lombok.extern.slf4j.Slf4j; +import me.zhengjie.annotation.DataPermission; +import me.zhengjie.annotation.Query; +import javax.persistence.criteria.*; +import java.lang.reflect.Field; +import java.util.*; + +/** + * @author Zheng Jie + * @date 2019-6-4 14:59:48 + */ +@Slf4j +@SuppressWarnings({"unchecked","all"}) +public class QueryHelp { + + public static Predicate getPredicate(Root root, Q query, CriteriaBuilder cb) { + List list = new ArrayList<>(); + if(query == null){ + return cb.and(list.toArray(new Predicate[0])); + } + // 数据权限验证 + DataPermission permission = query.getClass().getAnnotation(DataPermission.class); + if(permission != null){ + // 获取数据权限 + List dataScopes = SecurityUtils.getCurrentUserDataScope(); + if(CollectionUtil.isNotEmpty(dataScopes)){ + if(StringUtils.isNotBlank(permission.joinName()) && StringUtils.isNotBlank(permission.fieldName())) { + Join join = root.join(permission.joinName(), JoinType.LEFT); + list.add(getExpression(permission.fieldName(),join, root).in(dataScopes)); + } else if (StringUtils.isBlank(permission.joinName()) && StringUtils.isNotBlank(permission.fieldName())) { + list.add(getExpression(permission.fieldName(),null, root).in(dataScopes)); + } + } + } + try { + Map joinKey = new HashMap<>(); + List fields = getAllFields(query.getClass(), new ArrayList<>()); + for (Field field : fields) { + boolean accessible = field.isAccessible(); + // 设置对象的访问权限,保证对private的属性的访 + field.setAccessible(true); + Query q = field.getAnnotation(Query.class); + if (q != null) { + String propName = q.propName(); + String joinName = q.joinName(); + String blurry = q.blurry(); + String attributeName = isBlank(propName) ? field.getName() : propName; + Class fieldType = field.getType(); + Object val = field.get(query); + if (ObjectUtil.isNull(val) || "".equals(val)) { + continue; + } + Join join = null; + // 模糊多字段 + if (ObjectUtil.isNotEmpty(blurry)) { + String[] blurrys = blurry.split(","); + List orPredicate = new ArrayList<>(); + for (String s : blurrys) { + orPredicate.add(cb.like(root.get(s).as(String.class), "%" + val.toString() + "%")); + } + Predicate[] p = new Predicate[orPredicate.size()]; + list.add(cb.or(orPredicate.toArray(p))); + continue; + } + if (ObjectUtil.isNotEmpty(joinName)) { + join = joinKey.get(joinName); + if(join == null){ + String[] joinNames = joinName.split(">"); + for (String name : joinNames) { + switch (q.join()) { + case LEFT: + if(ObjectUtil.isNotNull(join) && ObjectUtil.isNotNull(val)){ + join = join.join(name, JoinType.LEFT); + } else { + join = root.join(name, JoinType.LEFT); + } + break; + case RIGHT: + if(ObjectUtil.isNotNull(join) && ObjectUtil.isNotNull(val)){ + join = join.join(name, JoinType.RIGHT); + } else { + join = root.join(name, JoinType.RIGHT); + } + break; + case INNER: + if(ObjectUtil.isNotNull(join) && ObjectUtil.isNotNull(val)){ + join = join.join(name, JoinType.INNER); + } else { + join = root.join(name, JoinType.INNER); + } + break; + default: break; + } + } + joinKey.put(joinName, join); + } + } + switch (q.type()) { + case EQUAL: + list.add(cb.equal(getExpression(attributeName,join,root) + .as((Class) fieldType),val)); + break; + case GREATER_THAN: + list.add(cb.greaterThanOrEqualTo(getExpression(attributeName,join,root) + .as((Class) fieldType), (Comparable) val)); + break; + case LESS_THAN: + list.add(cb.lessThanOrEqualTo(getExpression(attributeName,join,root) + .as((Class) fieldType), (Comparable) val)); + break; + case LESS_THAN_NQ: + list.add(cb.lessThan(getExpression(attributeName,join,root) + .as((Class) fieldType), (Comparable) val)); + break; + case INNER_LIKE: + list.add(cb.like(getExpression(attributeName,join,root) + .as(String.class), "%" + val.toString() + "%")); + break; + case LEFT_LIKE: + list.add(cb.like(getExpression(attributeName,join,root) + .as(String.class), "%" + val.toString())); + break; + case RIGHT_LIKE: + list.add(cb.like(getExpression(attributeName,join,root) + .as(String.class), val.toString() + "%")); + break; + case IN: + if (CollUtil.isNotEmpty((Collection)val)) { + list.add(getExpression(attributeName,join,root).in((Collection) val)); + } + break; + case NOT_IN: + if (CollUtil.isNotEmpty((Collection)val)) { + list.add(getExpression(attributeName,join,root).in((Collection) val).not()); + } + break; + case NOT_EQUAL: + list.add(cb.notEqual(getExpression(attributeName,join,root), val)); + break; + case NOT_NULL: + list.add(cb.isNotNull(getExpression(attributeName,join,root))); + break; + case IS_NULL: + list.add(cb.isNull(getExpression(attributeName,join,root))); + break; + case BETWEEN: + List between = new ArrayList<>((List)val); + if(between.size() == 2){ + list.add(cb.between(getExpression(attributeName, join, root).as((Class) between.get(0).getClass()), + (Comparable) between.get(0), (Comparable) between.get(1))); + } + break; + case FIND_IN_SET: + list.add(cb.greaterThan(cb.function("FIND_IN_SET", Integer.class, + cb.literal(val.toString()), root.get(attributeName)), 0)); + break; + default: break; + } + } + field.setAccessible(accessible); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + int size = list.size(); + return cb.and(list.toArray(new Predicate[size])); + } + + @SuppressWarnings("unchecked") + private static Expression getExpression(String attributeName, Join join, Root root) { + if (ObjectUtil.isNotEmpty(join)) { + return join.get(attributeName); + } else { + return root.get(attributeName); + } + } + + private static boolean isBlank(final CharSequence cs) { + int strLen; + if (cs == null || (strLen = cs.length()) == 0) { + return true; + } + for (int i = 0; i < strLen; i++) { + if (!Character.isWhitespace(cs.charAt(i))) { + return false; + } + } + return true; + } + + public static List getAllFields(Class clazz, List fields) { + if (clazz != null) { + fields.addAll(Arrays.asList(clazz.getDeclaredFields())); + getAllFields(clazz.getSuperclass(), fields); + } + return fields; + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/RedisUtils.java b/eladmin-common/src/main/java/me/zhengjie/utils/RedisUtils.java new file mode 100644 index 0000000..65d0278 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/utils/RedisUtils.java @@ -0,0 +1,725 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.utils; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.*; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.stereotype.Component; +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * @author / + */ +@Component +@SuppressWarnings({"unchecked", "all"}) +public class RedisUtils { + private static final Logger log = LoggerFactory.getLogger(RedisUtils.class); + + private RedisTemplate redisTemplate; + + public RedisUtils(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + this.redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + this.redisTemplate.setKeySerializer(new StringRedisSerializer()); + this.redisTemplate.setStringSerializer(new StringRedisSerializer()); + } + + /** + * 指定缓存失效时间 + * + * @param key 键 + * @param time 时间(秒) 注意:这里将会替换原有的时间 + */ + public boolean expire(String key, long time) { + try { + if (time > 0) { + redisTemplate.expire(key, time, TimeUnit.SECONDS); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + return true; + } + + /** + * 指定缓存失效时间 + * + * @param key 键 + * @param time 时间(秒) 注意:这里将会替换原有的时间 + * @param timeUnit 单位 + */ + public boolean expire(String key, long time, TimeUnit timeUnit) { + try { + if (time > 0) { + redisTemplate.expire(key, time, timeUnit); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + return true; + } + + /** + * 根据 key 获取过期时间 + * + * @param key 键 不能为null + * @return 时间(秒) 返回0代表为永久有效 + */ + public long getExpire(Object key) { + return redisTemplate.getExpire(key, TimeUnit.SECONDS); + } + + /** + * 查找匹配key + * + * @param pattern key + * @return / + */ + public List scan(String pattern) { + ScanOptions options = ScanOptions.scanOptions().match(pattern).build(); + RedisConnectionFactory factory = redisTemplate.getConnectionFactory(); + RedisConnection rc = Objects.requireNonNull(factory).getConnection(); + Cursor cursor = rc.scan(options); + List result = new ArrayList<>(); + while (cursor.hasNext()) { + result.add(new String(cursor.next())); + } + try { + RedisConnectionUtils.releaseConnection(rc, factory); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return result; + } + + /** + * 分页查询 key + * + * @param patternKey key + * @param page 页码 + * @param size 每页数目 + * @return / + */ + public List findKeysForPage(String patternKey, int page, int size) { + ScanOptions options = ScanOptions.scanOptions().match(patternKey).build(); + RedisConnectionFactory factory = redisTemplate.getConnectionFactory(); + RedisConnection rc = Objects.requireNonNull(factory).getConnection(); + Cursor cursor = rc.scan(options); + List result = new ArrayList<>(size); + int tmpIndex = 0; + int fromIndex = page * size; + int toIndex = page * size + size; + while (cursor.hasNext()) { + if (tmpIndex >= fromIndex && tmpIndex < toIndex) { + result.add(new String(cursor.next())); + tmpIndex++; + continue; + } + // 获取到满足条件的数据后,就可以退出了 + if (tmpIndex >= toIndex) { + break; + } + tmpIndex++; + cursor.next(); + } + try { + RedisConnectionUtils.releaseConnection(rc, factory); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return result; + } + + /** + * 判断key是否存在 + * + * @param key 键 + * @return true 存在 false不存在 + */ + public boolean hasKey(String key) { + try { + return redisTemplate.hasKey(key); + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 删除缓存 + * + * @param key 可以传一个值 或多个 + */ + public void del(String... keys) { + if (keys != null && keys.length > 0) { + if (keys.length == 1) { + boolean result = redisTemplate.delete(keys[0]); + log.debug("--------------------------------------------"); + log.debug(new StringBuilder("删除缓存:").append(keys[0]).append(",结果:").append(result).toString()); + log.debug("--------------------------------------------"); + } else { + Set keySet = new HashSet<>(); + for (String key : keys) { + if (redisTemplate.hasKey(key)) + keySet.add(key); + } + long count = redisTemplate.delete(keySet); + log.debug("--------------------------------------------"); + log.debug("成功删除缓存:" + keySet.toString()); + log.debug("缓存删除数量:" + count + "个"); + log.debug("--------------------------------------------"); + } + } + } + + /** + * 批量模糊删除key + * @param pattern + */ + public void scanDel(String pattern){ + ScanOptions options = ScanOptions.scanOptions().match(pattern).build(); + try (Cursor cursor = redisTemplate.executeWithStickyConnection( + (RedisCallback>) connection -> (Cursor) new ConvertingCursor<>( + connection.scan(options), redisTemplate.getKeySerializer()::deserialize))) { + while (cursor.hasNext()) { + redisTemplate.delete(cursor.next()); + } + } + } + + // ============================String============================= + + /** + * 普通缓存获取 + * + * @param key 键 + * @return 值 + */ + public Object get(String key) { + return key == null ? null : redisTemplate.opsForValue().get(key); + } + + /** + * 批量获取 + * + * @param keys + * @return + */ + public List multiGet(List keys) { + List list = redisTemplate.opsForValue().multiGet(Sets.newHashSet(keys)); + List resultList = Lists.newArrayList(); + Optional.ofNullable(list).ifPresent(e-> list.forEach(ele-> Optional.ofNullable(ele).ifPresent(resultList::add))); + return resultList; + } + + /** + * 普通缓存放入 + * + * @param key 键 + * @param value 值 + * @return true成功 false失败 + */ + public boolean set(String key, Object value) { + try { + redisTemplate.opsForValue().set(key, value); + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 普通缓存放入并设置时间 + * + * @param key 键 + * @param value 值 + * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期,注意:这里将会替换原有的时间 + * @return true成功 false 失败 + */ + public boolean set(String key, Object value, long time) { + try { + if (time > 0) { + redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); + } else { + set(key, value); + } + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 普通缓存放入并设置时间 + * + * @param key 键 + * @param value 值 + * @param time 时间,注意:这里将会替换原有的时间 + * @param timeUnit 类型 + * @return true成功 false 失败 + */ + public boolean set(String key, Object value, long time, TimeUnit timeUnit) { + try { + if (time > 0) { + redisTemplate.opsForValue().set(key, value, time, timeUnit); + } else { + set(key, value); + } + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + // ================================Map================================= + + /** + * HashGet + * + * @param key 键 不能为null + * @param item 项 不能为null + * @return 值 + */ + public Object hget(String key, String item) { + return redisTemplate.opsForHash().get(key, item); + } + + /** + * 获取hashKey对应的所有键值 + * + * @param key 键 + * @return 对应的多个键值 + */ + public Map hmget(String key) { + return redisTemplate.opsForHash().entries(key); + + } + + /** + * HashSet + * + * @param key 键 + * @param map 对应多个键值 + * @return true 成功 false 失败 + */ + public boolean hmset(String key, Map map) { + try { + redisTemplate.opsForHash().putAll(key, map); + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * HashSet + * + * @param key 键 + * @param map 对应多个键值 + * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间 + * @return true成功 false失败 + */ + public boolean hmset(String key, Map map, long time) { + try { + redisTemplate.opsForHash().putAll(key, map); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 向一张hash表中放入数据,如果不存在将创建 + * + * @param key 键 + * @param item 项 + * @param value 值 + * @return true 成功 false失败 + */ + public boolean hset(String key, String item, Object value) { + try { + redisTemplate.opsForHash().put(key, item, value); + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 向一张hash表中放入数据,如果不存在将创建 + * + * @param key 键 + * @param item 项 + * @param value 值 + * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间 + * @return true 成功 false失败 + */ + public boolean hset(String key, String item, Object value, long time) { + try { + redisTemplate.opsForHash().put(key, item, value); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 删除hash表中的值 + * + * @param key 键 不能为null + * @param item 项 可以使多个 不能为null + */ + public void hdel(String key, Object... item) { + redisTemplate.opsForHash().delete(key, item); + } + + /** + * 判断hash表中是否有该项的值 + * + * @param key 键 不能为null + * @param item 项 不能为null + * @return true 存在 false不存在 + */ + public boolean hHasKey(String key, String item) { + return redisTemplate.opsForHash().hasKey(key, item); + } + + /** + * hash递增 如果不存在,就会创建一个 并把新增后的值返回 + * + * @param key 键 + * @param item 项 + * @param by 要增加几(大于0) + * @return + */ + public double hincr(String key, String item, double by) { + return redisTemplate.opsForHash().increment(key, item, by); + } + + /** + * hash递减 + * + * @param key 键 + * @param item 项 + * @param by 要减少记(小于0) + * @return + */ + public double hdecr(String key, String item, double by) { + return redisTemplate.opsForHash().increment(key, item, -by); + } + + // ============================set============================= + + /** + * 根据key获取Set中的所有值 + * + * @param key 键 + * @return + */ + public Set sGet(String key) { + try { + return redisTemplate.opsForSet().members(key); + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } + } + + /** + * 根据value从一个set中查询,是否存在 + * + * @param key 键 + * @param value 值 + * @return true 存在 false不存在 + */ + public boolean sHasKey(String key, Object value) { + try { + return redisTemplate.opsForSet().isMember(key, value); + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 将数据放入set缓存 + * + * @param key 键 + * @param values 值 可以是多个 + * @return 成功个数 + */ + public long sSet(String key, Object... values) { + try { + return redisTemplate.opsForSet().add(key, values); + } catch (Exception e) { + log.error(e.getMessage(), e); + return 0; + } + } + + /** + * 将set数据放入缓存 + * + * @param key 键 + * @param time 时间(秒) 注意:这里将会替换原有的时间 + * @param values 值 可以是多个 + * @return 成功个数 + */ + public long sSetAndTime(String key, long time, Object... values) { + try { + Long count = redisTemplate.opsForSet().add(key, values); + if (time > 0) { + expire(key, time); + } + return count; + } catch (Exception e) { + log.error(e.getMessage(), e); + return 0; + } + } + + /** + * 获取set缓存的长度 + * + * @param key 键 + * @return + */ + public long sGetSetSize(String key) { + try { + return redisTemplate.opsForSet().size(key); + } catch (Exception e) { + log.error(e.getMessage(), e); + return 0; + } + } + + /** + * 移除值为value的 + * + * @param key 键 + * @param values 值 可以是多个 + * @return 移除的个数 + */ + public long setRemove(String key, Object... values) { + try { + Long count = redisTemplate.opsForSet().remove(key, values); + return count; + } catch (Exception e) { + log.error(e.getMessage(), e); + return 0; + } + } + + // ===============================list================================= + + /** + * 获取list缓存的内容 + * + * @param key 键 + * @param start 开始 + * @param end 结束 0 到 -1代表所有值 + * @return + */ + public List lGet(String key, long start, long end) { + try { + return redisTemplate.opsForList().range(key, start, end); + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } + } + + /** + * 获取list缓存的长度 + * + * @param key 键 + * @return + */ + public long lGetListSize(String key) { + try { + return redisTemplate.opsForList().size(key); + } catch (Exception e) { + log.error(e.getMessage(), e); + return 0; + } + } + + /** + * 通过索引 获取list中的值 + * + * @param key 键 + * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推 + * @return + */ + public Object lGetIndex(String key, long index) { + try { + return redisTemplate.opsForList().index(key, index); + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @return + */ + public boolean lSet(String key, Object value) { + try { + redisTemplate.opsForList().rightPush(key, value); + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @param time 时间(秒) 注意:这里将会替换原有的时间 + * @return + */ + public boolean lSet(String key, Object value, long time) { + try { + redisTemplate.opsForList().rightPush(key, value); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @return + */ + public boolean lSet(String key, List value) { + try { + redisTemplate.opsForList().rightPushAll(key, value); + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @param time 时间(秒) 注意:这里将会替换原有的时间 + * @return + */ + public boolean lSet(String key, List value, long time) { + try { + redisTemplate.opsForList().rightPushAll(key, value); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 根据索引修改list中的某条数据 + * + * @param key 键 + * @param index 索引 + * @param value 值 + * @return / + */ + public boolean lUpdateIndex(String key, long index, Object value) { + try { + redisTemplate.opsForList().set(key, index, value); + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 移除N个值为value + * + * @param key 键 + * @param count 移除多少个 + * @param value 值 + * @return 移除的个数 + */ + public long lRemove(String key, long count, Object value) { + try { + return redisTemplate.opsForList().remove(key, count, value); + } catch (Exception e) { + log.error(e.getMessage(), e); + return 0; + } + } + + /** + * @param prefix 前缀 + * @param ids id + */ + public void delByKeys(String prefix, Set ids) { + Set keys = new HashSet<>(); + for (Long id : ids) { + keys.addAll(redisTemplate.keys(new StringBuffer(prefix).append(id).toString())); + } + long count = redisTemplate.delete(keys); + // 此处提示可自行删除 + log.debug("--------------------------------------------"); + log.debug("成功删除缓存:" + keys.toString()); + log.debug("缓存删除数量:" + count + "个"); + log.debug("--------------------------------------------"); + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/RequestHolder.java b/eladmin-common/src/main/java/me/zhengjie/utils/RequestHolder.java new file mode 100644 index 0000000..71a4b9e --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/utils/RequestHolder.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.utils; + +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import javax.servlet.http.HttpServletRequest; +import java.util.Objects; + +/** + * 获取 HttpServletRequest + * @author Zheng Jie + * @date 2018-11-24 + */ +public class RequestHolder { + + public static HttpServletRequest getHttpServletRequest() { + return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/RsaUtils.java b/eladmin-common/src/main/java/me/zhengjie/utils/RsaUtils.java new file mode 100644 index 0000000..8123ff3 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/utils/RsaUtils.java @@ -0,0 +1,198 @@ +package me.zhengjie.utils; + +import org.apache.commons.codec.binary.Base64; +import javax.crypto.Cipher; +import java.io.ByteArrayOutputStream; +import java.security.*; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +/** + * @author https://www.cnblogs.com/nihaorz/p/10690643.html + * @description Rsa 工具类,公钥私钥生成,加解密 + * @date 2020-05-18 + **/ +public class RsaUtils { + + private static final String SRC = "123456"; + + public static void main(String[] args) throws Exception { + System.out.println("\n"); + RsaKeyPair keyPair = generateKeyPair(); + System.out.println("公钥:" + keyPair.getPublicKey()); + System.out.println("私钥:" + keyPair.getPrivateKey()); + System.out.println("\n"); + test1(keyPair); + System.out.println("\n"); + test2(keyPair); + System.out.println("\n"); + } + + /** + * 公钥加密私钥解密 + */ + private static void test1(RsaKeyPair keyPair) throws Exception { + System.out.println("***************** 公钥加密私钥解密开始 *****************"); + String text1 = encryptByPublicKey(keyPair.getPublicKey(), RsaUtils.SRC); + String text2 = decryptByPrivateKey(keyPair.getPrivateKey(), text1); + System.out.println("加密前:" + RsaUtils.SRC); + System.out.println("加密后:" + text1); + System.out.println("解密后:" + text2); + if (RsaUtils.SRC.equals(text2)) { + System.out.println("解密字符串和原始字符串一致,解密成功"); + } else { + System.out.println("解密字符串和原始字符串不一致,解密失败"); + } + System.out.println("***************** 公钥加密私钥解密结束 *****************"); + } + + /** + * 私钥加密公钥解密 + * @throws Exception / + */ + private static void test2(RsaKeyPair keyPair) throws Exception { + System.out.println("***************** 私钥加密公钥解密开始 *****************"); + String text1 = encryptByPrivateKey(keyPair.getPrivateKey(), RsaUtils.SRC); + String text2 = decryptByPublicKey(keyPair.getPublicKey(), text1); + System.out.println("加密前:" + RsaUtils.SRC); + System.out.println("加密后:" + text1); + System.out.println("解密后:" + text2); + if (RsaUtils.SRC.equals(text2)) { + System.out.println("解密字符串和原始字符串一致,解密成功"); + } else { + System.out.println("解密字符串和原始字符串不一致,解密失败"); + } + System.out.println("***************** 私钥加密公钥解密结束 *****************"); + } + + /** + * 公钥解密 + * + * @param publicKeyText 公钥 + * @param text 待解密的信息 + * @return / + * @throws Exception / + */ + public static String decryptByPublicKey(String publicKeyText, String text) throws Exception { + X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyText)); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec); + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.DECRYPT_MODE, publicKey); + byte[] result = doLongerCipherFinal(Cipher.DECRYPT_MODE, cipher, Base64.decodeBase64(text)); + return new String(result); + } + + /** + * 私钥加密 + * + * @param privateKeyText 私钥 + * @param text 待加密的信息 + * @return / + * @throws Exception / + */ + public static String encryptByPrivateKey(String privateKeyText, String text) throws Exception { + PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyText)); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec); + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.ENCRYPT_MODE, privateKey); + byte[] result = doLongerCipherFinal(Cipher.ENCRYPT_MODE, cipher, text.getBytes()); + return Base64.encodeBase64String(result); + } + + /** + * 私钥解密 + * + * @param privateKeyText 私钥 + * @param text 待解密的文本 + * @return / + * @throws Exception / + */ + public static String decryptByPrivateKey(String privateKeyText, String text) throws Exception { + PKCS8EncodedKeySpec pkcs8EncodedKeySpec5 = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyText)); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec5); + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + byte[] result = doLongerCipherFinal(Cipher.DECRYPT_MODE, cipher, Base64.decodeBase64(text)); + return new String(result); + } + + /** + * 公钥加密 + * + * @param publicKeyText 公钥 + * @param text 待加密的文本 + * @return / + */ + public static String encryptByPublicKey(String publicKeyText, String text) throws Exception { + X509EncodedKeySpec x509EncodedKeySpec2 = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyText)); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec2); + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + byte[] result = doLongerCipherFinal(Cipher.ENCRYPT_MODE, cipher, text.getBytes()); + return Base64.encodeBase64String(result); + } + + private static byte[] doLongerCipherFinal(int opMode,Cipher cipher, byte[] source) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + if (opMode == Cipher.DECRYPT_MODE) { + out.write(cipher.doFinal(source)); + } else { + int offset = 0; + int totalSize = source.length; + while (totalSize - offset > 0) { + int size = Math.min(cipher.getOutputSize(0) - 11, totalSize - offset); + out.write(cipher.doFinal(source, offset, size)); + offset += size; + } + } + out.close(); + return out.toByteArray(); + } + + /** + * 构建RSA密钥对 + * + * @return / + * @throws NoSuchAlgorithmException / + */ + public static RsaKeyPair generateKeyPair() throws NoSuchAlgorithmException { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(1024); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate(); + String publicKeyString = Base64.encodeBase64String(rsaPublicKey.getEncoded()); + String privateKeyString = Base64.encodeBase64String(rsaPrivateKey.getEncoded()); + return new RsaKeyPair(publicKeyString, privateKeyString); + } + + + /** + * RSA密钥对对象 + */ + public static class RsaKeyPair { + + private final String publicKey; + private final String privateKey; + + public RsaKeyPair(String publicKey, String privateKey) { + this.publicKey = publicKey; + this.privateKey = privateKey; + } + + public String getPublicKey() { + return publicKey; + } + + public String getPrivateKey() { + return privateKey; + } + + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/SecurityUtils.java b/eladmin-common/src/main/java/me/zhengjie/utils/SecurityUtils.java new file mode 100644 index 0000000..28e4672 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/utils/SecurityUtils.java @@ -0,0 +1,99 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.utils; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import lombok.extern.slf4j.Slf4j; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.utils.enums.DataScopeEnum; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import java.util.List; + +/** + * 获取当前登录的用户 + * @author Zheng Jie + * @date 2019-01-17 + */ +@Slf4j +public class SecurityUtils { + + /** + * 获取当前登录的用户 + * @return UserDetails + */ + public static UserDetails getCurrentUser() { + UserDetailsService userDetailsService = SpringContextHolder.getBean(UserDetailsService.class); + return userDetailsService.loadUserByUsername(getCurrentUsername()); + } + + /** + * 获取系统用户名称 + * + * @return 系统用户名称 + */ + public static String getCurrentUsername() { + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + throw new BadRequestException(HttpStatus.UNAUTHORIZED, "当前登录状态过期"); + } + if (authentication.getPrincipal() instanceof UserDetails) { + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + return userDetails.getUsername(); + } + throw new BadRequestException(HttpStatus.UNAUTHORIZED, "找不到当前登录的信息"); + } + + /** + * 获取系统用户ID + * @return 系统用户ID + */ + public static Long getCurrentUserId() { + UserDetails userDetails = getCurrentUser(); + // 将 Java 对象转换为 JSONObject 对象 + JSONObject jsonObject = (JSONObject) JSON.toJSON(userDetails); + return jsonObject.getJSONObject("user").getLong("id"); + } + + /** + * 获取当前用户的数据权限 + * @return / + */ + public static List getCurrentUserDataScope(){ + UserDetails userDetails = getCurrentUser(); + // 将 Java 对象转换为 JSONObject 对象 + JSONObject jsonObject = (JSONObject) JSON.toJSON(userDetails); + JSONArray jsonArray = jsonObject.getJSONArray("dataScopes"); + return JSON.parseArray(jsonArray.toJSONString(), Long.class); + } + + /** + * 获取数据权限级别 + * @return 级别 + */ + public static String getDataScopeType() { + List dataScopes = getCurrentUserDataScope(); + if(dataScopes.size() != 0){ + return ""; + } + return DataScopeEnum.ALL.getValue(); + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/SpringContextHolder.java b/eladmin-common/src/main/java/me/zhengjie/utils/SpringContextHolder.java new file mode 100644 index 0000000..41ead38 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/utils/SpringContextHolder.java @@ -0,0 +1,156 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.utils; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @author Jie + * @date 2019-01-07 + */ +@Slf4j +public class SpringContextHolder implements ApplicationContextAware, DisposableBean { + + private static ApplicationContext applicationContext = null; + private static final List CALL_BACKS = new ArrayList<>(); + private static boolean addCallback = true; + + /** + * 针对 某些初始化方法,在SpringContextHolder 未初始化时 提交回调方法。 + * 在SpringContextHolder 初始化后,进行回调使用 + * + * @param callBack 回调函数 + */ + public synchronized static void addCallBacks(CallBack callBack) { + if (addCallback) { + SpringContextHolder.CALL_BACKS.add(callBack); + } else { + log.warn("CallBack:{} 已无法添加!立即执行", callBack.getCallBackName()); + callBack.executor(); + } + } + + /** + * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型. + */ + @SuppressWarnings("unchecked") + public static T getBean(String name) { + assertContextInjected(); + return (T) applicationContext.getBean(name); + } + + /** + * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型. + */ + public static T getBean(Class requiredType) { + assertContextInjected(); + return applicationContext.getBean(requiredType); + } + + /** + * 获取SpringBoot 配置信息 + * + * @param property 属性key + * @param defaultValue 默认值 + * @param requiredType 返回类型 + * @return / + */ + public static T getProperties(String property, T defaultValue, Class requiredType) { + T result = defaultValue; + try { + result = getBean(Environment.class).getProperty(property, requiredType); + } catch (Exception ignored) {} + return result; + } + + /** + * 获取SpringBoot 配置信息 + * + * @param property 属性key + * @return / + */ + public static String getProperties(String property) { + return getProperties(property, null, String.class); + } + + /** + * 获取SpringBoot 配置信息 + * + * @param property 属性key + * @param requiredType 返回类型 + * @return / + */ + public static T getProperties(String property, Class requiredType) { + return getProperties(property, null, requiredType); + } + + /** + * 检查ApplicationContext不为空. + */ + private static void assertContextInjected() { + if (applicationContext == null) { + throw new IllegalStateException("applicaitonContext属性未注入, 请在applicationContext" + + ".xml中定义SpringContextHolder或在SpringBoot启动类中注册SpringContextHolder."); + } + } + + /** + * 清除SpringContextHolder中的ApplicationContext为Null. + */ + private static void clearHolder() { + log.debug("清除SpringContextHolder中的ApplicationContext:" + + applicationContext); + applicationContext = null; + } + + @Override + public void destroy() { + SpringContextHolder.clearHolder(); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + if (SpringContextHolder.applicationContext != null) { + log.warn("SpringContextHolder中的ApplicationContext被覆盖, 原有ApplicationContext为:" + SpringContextHolder.applicationContext); + } + SpringContextHolder.applicationContext = applicationContext; + if (addCallback) { + for (CallBack callBack : SpringContextHolder.CALL_BACKS) { + callBack.executor(); + } + CALL_BACKS.clear(); + } + SpringContextHolder.addCallback = false; + } + + /** + * 获取 @Service 的所有 bean 名称 + * @return / + */ + public static List getAllServiceBeanName() { + return new ArrayList<>(Arrays.asList(applicationContext + .getBeanNamesForAnnotation(Service.class))); + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/StringUtils.java b/eladmin-common/src/main/java/me/zhengjie/utils/StringUtils.java new file mode 100644 index 0000000..a27a47a --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/utils/StringUtils.java @@ -0,0 +1,232 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.utils; + +import cn.hutool.http.useragent.UserAgent; +import cn.hutool.http.useragent.UserAgentUtil; +import lombok.extern.slf4j.Slf4j; +import net.dreamlu.mica.ip2region.core.Ip2regionSearcher; +import net.dreamlu.mica.ip2region.core.IpInfo; +import javax.servlet.http.HttpServletRequest; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.UnknownHostException; +import java.util.Calendar; +import java.util.Date; +import java.util.Enumeration; + +/** + * @author Zheng Jie + * 字符串工具类, 继承org.apache.commons.lang3.StringUtils类 + */ +@Slf4j +public class StringUtils extends org.apache.commons.lang3.StringUtils { + + private static final char SEPARATOR = '_'; + private static final String UNKNOWN = "unknown"; + + /** + * 注入bean + */ + private final static Ip2regionSearcher IP_SEARCHER = SpringContextHolder.getBean(Ip2regionSearcher.class); + + /** + * 驼峰命名法工具 + * + * @return toCamelCase(" hello_world ") == "helloWorld" + * toCapitalizeCamelCase("hello_world") == "HelloWorld" + * toUnderScoreCase("helloWorld") = "hello_world" + */ + public static String toCamelCase(String s) { + if (s == null) { + return null; + } + + s = s.toLowerCase(); + + StringBuilder sb = new StringBuilder(s.length()); + boolean upperCase = false; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + + if (c == SEPARATOR) { + upperCase = true; + } else if (upperCase) { + sb.append(Character.toUpperCase(c)); + upperCase = false; + } else { + sb.append(c); + } + } + + return sb.toString(); + } + + /** + * 驼峰命名法工具 + * + * @return toCamelCase(" hello_world ") == "helloWorld" + * toCapitalizeCamelCase("hello_world") == "HelloWorld" + * toUnderScoreCase("helloWorld") = "hello_world" + */ + public static String toCapitalizeCamelCase(String s) { + if (s == null) { + return null; + } + s = toCamelCase(s); + return s.substring(0, 1).toUpperCase() + s.substring(1); + } + + /** + * 驼峰命名法工具 + * + * @return toCamelCase(" hello_world ") == "helloWorld" + * toCapitalizeCamelCase("hello_world") == "HelloWorld" + * toUnderScoreCase("helloWorld") = "hello_world" + */ + static String toUnderScoreCase(String s) { + if (s == null) { + return null; + } + + StringBuilder sb = new StringBuilder(); + boolean upperCase = false; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + + boolean nextUpperCase = true; + + if (i < (s.length() - 1)) { + nextUpperCase = Character.isUpperCase(s.charAt(i + 1)); + } + + if ((i > 0) && Character.isUpperCase(c)) { + if (!upperCase || !nextUpperCase) { + sb.append(SEPARATOR); + } + upperCase = true; + } else { + upperCase = false; + } + + sb.append(Character.toLowerCase(c)); + } + + return sb.toString(); + } + + /** + * 获取ip地址 + */ + public static String getIp(HttpServletRequest request) { + String ip = request.getHeader("x-forwarded-for"); + if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + String comma = ","; + String localhost = "127.0.0.1"; + if (ip.contains(comma)) { + ip = ip.split(",")[0]; + } + if (localhost.equals(ip)) { + // 获取本机真正的ip地址 + try { + ip = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error(e.getMessage(), e); + } + } + return ip; + } + + /** + * 根据ip获取详细地址 + */ + public static String getCityInfo(String ip) { + IpInfo ipInfo = IP_SEARCHER.memorySearch(ip); + if(ipInfo != null){ + return ipInfo.getAddress(); + } + return null; + } + + public static String getBrowser(HttpServletRequest request) { + UserAgent ua = UserAgentUtil.parse(request.getHeader("User-Agent")); + String browser = ua.getBrowser().toString() + " " + ua.getVersion(); + return browser.replace(".0.0.0",""); + } + + /** + * 获得当天是周几 + */ + public static String getWeekDay() { + String[] weekDays = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; + Calendar cal = Calendar.getInstance(); + cal.setTime(new Date()); + + int w = cal.get(Calendar.DAY_OF_WEEK) - 1; + if (w < 0) { + w = 0; + } + return weekDays[w]; + } + + /** + * 获取当前机器的IP + * + * @return / + */ + public static String getLocalIp() { + try { + InetAddress candidateAddress = null; + // 遍历所有的网络接口 + for (Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); interfaces.hasMoreElements();) { + NetworkInterface anInterface = interfaces.nextElement(); + // 在所有的接口下再遍历IP + for (Enumeration inetAddresses = anInterface.getInetAddresses(); inetAddresses.hasMoreElements();) { + InetAddress inetAddr = inetAddresses.nextElement(); + // 排除loopback类型地址 + if (!inetAddr.isLoopbackAddress()) { + if (inetAddr.isSiteLocalAddress()) { + // 如果是site-local地址,就是它了 + return inetAddr.getHostAddress(); + } else if (candidateAddress == null) { + // site-local类型的地址未被发现,先记录候选地址 + candidateAddress = inetAddr; + } + } + } + } + if (candidateAddress != null) { + return candidateAddress.getHostAddress(); + } + // 如果没有发现 non-loopback地址.只能用最次选的方案 + InetAddress jdkSuppliedAddress = InetAddress.getLocalHost(); + if (jdkSuppliedAddress == null) { + return ""; + } + return jdkSuppliedAddress.getHostAddress(); + } catch (Exception e) { + return ""; + } + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/ThrowableUtil.java b/eladmin-common/src/main/java/me/zhengjie/utils/ThrowableUtil.java new file mode 100644 index 0000000..075a65c --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/utils/ThrowableUtil.java @@ -0,0 +1,37 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.utils; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * 异常工具 2019-01-06 + * @author Zheng Jie + */ +public class ThrowableUtil { + + /** + * 获取堆栈信息 + */ + public static String getStackTrace(Throwable throwable){ + StringWriter sw = new StringWriter(); + try (PrintWriter pw = new PrintWriter(sw)) { + throwable.printStackTrace(pw); + return sw.toString(); + } + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/ValidationUtil.java b/eladmin-common/src/main/java/me/zhengjie/utils/ValidationUtil.java new file mode 100644 index 0000000..e561762 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/utils/ValidationUtil.java @@ -0,0 +1,46 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.utils; + +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.ObjectUtil; +import me.zhengjie.exception.BadRequestException; + +/** + * 验证工具 + * + * @author Zheng Jie + * @date 2018-11-23 + */ +public class ValidationUtil { + + /** + * 验证空 + */ + public static void isNull(Object obj, String entity, String parameter , Object value){ + if(ObjectUtil.isNull(obj)){ + String msg = entity + " 不存在: "+ parameter +" is "+ value; + throw new BadRequestException(msg); + } + } + + /** + * 验证是否为邮箱 + */ + public static boolean isEmail(String email) { + return Validator.isEmail(email); + } +} diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/enums/CodeBiEnum.java b/eladmin-common/src/main/java/me/zhengjie/utils/enums/CodeBiEnum.java new file mode 100644 index 0000000..944bc71 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/utils/enums/CodeBiEnum.java @@ -0,0 +1,50 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.utils.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + *

+ * 验证码业务场景 + *

+ * @author Zheng Jie + * @date 2020-05-02 + */ +@Getter +@AllArgsConstructor +public enum CodeBiEnum { + + /* 旧邮箱修改邮箱 */ + ONE(1, "旧邮箱修改邮箱"), + + /* 通过邮箱修改密码 */ + TWO(2, "通过邮箱修改密码"); + + private final Integer code; + private final String description; + + public static CodeBiEnum find(Integer code) { + for (CodeBiEnum value : CodeBiEnum.values()) { + if (value.getCode().equals(code)) { + return value; + } + } + return null; + } + +} diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/enums/CodeEnum.java b/eladmin-common/src/main/java/me/zhengjie/utils/enums/CodeEnum.java new file mode 100644 index 0000000..916862a --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/utils/enums/CodeEnum.java @@ -0,0 +1,46 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.utils.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + *

+ * 验证码业务场景对应的 Redis 中的 key + *

+ * @author Zheng Jie + * @date 2020-05-02 + */ +@Getter +@AllArgsConstructor +public enum CodeEnum { + + /* 通过手机号码重置邮箱 */ + PHONE_RESET_EMAIL_CODE("phone_reset_email_code_", "通过手机号码重置邮箱"), + + /* 通过旧邮箱重置邮箱 */ + EMAIL_RESET_EMAIL_CODE("email_reset_email_code_", "通过旧邮箱重置邮箱"), + + /* 通过手机号码重置密码 */ + PHONE_RESET_PWD_CODE("phone_reset_pwd_code_", "通过手机号码重置密码"), + + /* 通过邮箱重置密码 */ + EMAIL_RESET_PWD_CODE("email_reset_pwd_code_", "通过邮箱重置密码"); + + private final String key; + private final String description; +} diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/enums/DataScopeEnum.java b/eladmin-common/src/main/java/me/zhengjie/utils/enums/DataScopeEnum.java new file mode 100644 index 0000000..465eef6 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/utils/enums/DataScopeEnum.java @@ -0,0 +1,53 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.utils.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + *

+ * 数据权限枚举 + *

+ * @author Zheng Jie + * @date 2020-05-07 + */ +@Getter +@AllArgsConstructor +public enum DataScopeEnum { + + /* 全部的数据权限 */ + ALL("全部", "全部的数据权限"), + + /* 自己部门的数据权限 */ + THIS_LEVEL("本级", "自己部门的数据权限"), + + /* 自定义的数据权限 */ + CUSTOMIZE("自定义", "自定义的数据权限"); + + private final String value; + private final String description; + + public static DataScopeEnum find(String val) { + for (DataScopeEnum dataScopeEnum : DataScopeEnum.values()) { + if (dataScopeEnum.getValue().equals(val)) { + return dataScopeEnum; + } + } + return null; + } + +} diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/enums/RequestMethodEnum.java b/eladmin-common/src/main/java/me/zhengjie/utils/enums/RequestMethodEnum.java new file mode 100644 index 0000000..7de5146 --- /dev/null +++ b/eladmin-common/src/main/java/me/zhengjie/utils/enums/RequestMethodEnum.java @@ -0,0 +1,74 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.utils.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Zheng Jie + * @website https://eladmin.vip + * @description + * @date 2020-06-10 + **/ +@Getter +@AllArgsConstructor +public enum RequestMethodEnum { + + /** + * 搜寻 @AnonymousGetMapping + */ + GET("GET"), + + /** + * 搜寻 @AnonymousPostMapping + */ + POST("POST"), + + /** + * 搜寻 @AnonymousPutMapping + */ + PUT("PUT"), + + /** + * 搜寻 @AnonymousPatchMapping + */ + PATCH("PATCH"), + + /** + * 搜寻 @AnonymousDeleteMapping + */ + DELETE("DELETE"), + + /** + * 否则就是所有 Request 接口都放行 + */ + ALL("All"); + + /** + * Request 类型 + */ + private final String type; + + public static RequestMethodEnum find(String type) { + for (RequestMethodEnum value : RequestMethodEnum.values()) { + if (value.getType().equals(type)) { + return value; + } + } + return ALL; + } +} diff --git a/eladmin-common/src/test/java/me/zhengjie/utils/DateUtilsTest.java b/eladmin-common/src/test/java/me/zhengjie/utils/DateUtilsTest.java new file mode 100644 index 0000000..dfe01e0 --- /dev/null +++ b/eladmin-common/src/test/java/me/zhengjie/utils/DateUtilsTest.java @@ -0,0 +1,26 @@ +package me.zhengjie.utils; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.Date; + +public class DateUtilsTest { + @Test + public void test1() { + long l = System.currentTimeMillis() / 1000; + LocalDateTime localDateTime = DateUtil.fromTimeStamp(l); + System.out.print(DateUtil.localDateTimeFormatyMdHms(localDateTime)); + } + + @Test + public void test2() { + LocalDateTime now = LocalDateTime.now(); + System.out.println(DateUtil.localDateTimeFormatyMdHms(now)); + Date date = DateUtil.toDate(now); + LocalDateTime localDateTime = DateUtil.toLocalDateTime(date); + System.out.println(DateUtil.localDateTimeFormatyMdHms(localDateTime)); + LocalDateTime localDateTime1 = DateUtil.fromTimeStamp(date.getTime() / 1000); + System.out.println(DateUtil.localDateTimeFormatyMdHms(localDateTime1)); + } +} diff --git a/eladmin-common/src/test/java/me/zhengjie/utils/EncryptUtilsTest.java b/eladmin-common/src/test/java/me/zhengjie/utils/EncryptUtilsTest.java new file mode 100644 index 0000000..3ec7375 --- /dev/null +++ b/eladmin-common/src/test/java/me/zhengjie/utils/EncryptUtilsTest.java @@ -0,0 +1,33 @@ +package me.zhengjie.utils; + +import org.junit.jupiter.api.Test; + +import static me.zhengjie.utils.EncryptUtils.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class EncryptUtilsTest { + + /** + * 对称加密 + */ + @Test + public void testDesEncrypt() { + try { + assertEquals("7772841DC6099402", desEncrypt("123456")); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 对称解密 + */ + @Test + public void testDesDecrypt() { + try { + assertEquals("123456", desDecrypt("7772841DC6099402")); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/eladmin-common/src/test/java/me/zhengjie/utils/FileUtilTest.java b/eladmin-common/src/test/java/me/zhengjie/utils/FileUtilTest.java new file mode 100644 index 0000000..48e06bd --- /dev/null +++ b/eladmin-common/src/test/java/me/zhengjie/utils/FileUtilTest.java @@ -0,0 +1,36 @@ +package me.zhengjie.utils; + +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockMultipartFile; + +import static me.zhengjie.utils.FileUtil.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class FileUtilTest { + + @Test + public void testToFile() { + long retval = toFile(new MockMultipartFile("foo", (byte[]) null)).getTotalSpace(); + assertEquals(500695072768L, retval); + } + + @Test + public void testGetExtensionName() { + assertEquals("foo", getExtensionName("foo")); + assertEquals("exe", getExtensionName("bar.exe")); + } + + @Test + public void testGetFileNameNoEx() { + assertEquals("foo", getFileNameNoEx("foo")); + assertEquals("bar", getFileNameNoEx("bar.txt")); + } + + @Test + public void testGetSize() { + assertEquals("1000B ", getSize(1000)); + assertEquals("1.00KB ", getSize(1024)); + assertEquals("1.00MB ", getSize(1048576)); + assertEquals("1.00GB ", getSize(1073741824)); + } +} diff --git a/eladmin-common/src/test/java/me/zhengjie/utils/StringUtilsTest.java b/eladmin-common/src/test/java/me/zhengjie/utils/StringUtilsTest.java new file mode 100644 index 0000000..ffb2cf8 --- /dev/null +++ b/eladmin-common/src/test/java/me/zhengjie/utils/StringUtilsTest.java @@ -0,0 +1,48 @@ +package me.zhengjie.utils; + +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import static me.zhengjie.utils.StringUtils.getIp; +import static me.zhengjie.utils.StringUtils.getWeekDay; +import static me.zhengjie.utils.StringUtils.toCamelCase; +import static me.zhengjie.utils.StringUtils.toCapitalizeCamelCase; +import static me.zhengjie.utils.StringUtils.toUnderScoreCase; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class StringUtilsTest { + + @Test + public void testToCamelCase() { + assertNull(toCamelCase(null)); + } + + @Test + public void testToCapitalizeCamelCase() { + assertNull(StringUtils.toCapitalizeCamelCase(null)); + assertEquals("HelloWorld", toCapitalizeCamelCase("hello_world")); + } + + @Test + public void testToUnderScoreCase() { + assertNull(StringUtils.toUnderScoreCase(null)); + assertEquals("hello_world", toUnderScoreCase("helloWorld")); + assertEquals("\u0000\u0000", toUnderScoreCase("\u0000\u0000")); + assertEquals("\u0000_a", toUnderScoreCase("\u0000A")); + } + + @Test + public void testGetWeekDay() { + SimpleDateFormat simpleDateformat = new SimpleDateFormat("E"); + assertEquals(simpleDateformat.format(new Date()), getWeekDay()); + } + + @Test + public void testGetIP() { + assertEquals("127.0.0.1", getIp(new MockHttpServletRequest())); + } +} diff --git a/eladmin-generator/pom.xml b/eladmin-generator/pom.xml new file mode 100644 index 0000000..3584959 --- /dev/null +++ b/eladmin-generator/pom.xml @@ -0,0 +1,39 @@ + + + + eladmin + me.zhengjie + 2.7 + + 4.0.0 + + eladmin-generator + 代码生成模块 + + + 1.10 + + + + + me.zhengjie + eladmin-common + 2.7 + + + + + org.springframework.boot + spring-boot-starter-freemarker + + + + + commons-configuration + commons-configuration + ${configuration.version} + + + \ No newline at end of file diff --git a/eladmin-generator/src/main/java/me/zhengjie/domain/ColumnInfo.java b/eladmin-generator/src/main/java/me/zhengjie/domain/ColumnInfo.java new file mode 100644 index 0000000..fdb962a --- /dev/null +++ b/eladmin-generator/src/main/java/me/zhengjie/domain/ColumnInfo.java @@ -0,0 +1,97 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.domain; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import me.zhengjie.utils.GenUtil; +import javax.persistence.*; +import java.io.Serializable; + +/** + * 列的数据信息 + * @author Zheng Jie + * @date 2019-01-02 + */ +@Getter +@Setter +@Entity +@NoArgsConstructor +@Table(name = "code_column_config") +public class ColumnInfo implements Serializable { + + @Id + @Column(name = "column_id") + @ApiModelProperty(value = "ID", hidden = true) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ApiModelProperty(value = "表名") + private String tableName; + + @ApiModelProperty(value = "数据库字段名称") + private String columnName; + + @ApiModelProperty(value = "数据库字段类型") + private String columnType; + + @ApiModelProperty(value = "数据库字段键类型") + private String keyType; + + @ApiModelProperty(value = "字段额外的参数") + private String extra; + + @ApiModelProperty(value = "数据库字段描述") + private String remark; + + @ApiModelProperty(value = "是否必填") + private Boolean notNull; + + @ApiModelProperty(value = "是否在列表显示") + private Boolean listShow; + + @ApiModelProperty(value = "是否表单显示") + private Boolean formShow; + + @ApiModelProperty(value = "表单类型") + private String formType; + + @ApiModelProperty(value = "查询 1:模糊 2:精确") + private String queryType; + + @ApiModelProperty(value = "字典名称") + private String dictName; + + @ApiModelProperty(value = "日期注解") + private String dateAnnotation; + + public ColumnInfo(String tableName, String columnName, Boolean notNull, String columnType, String remark, String keyType, String extra) { + this.tableName = tableName; + this.columnName = columnName; + this.columnType = columnType; + this.keyType = keyType; + this.extra = extra; + this.notNull = notNull; + if(GenUtil.PK.equalsIgnoreCase(keyType) && GenUtil.EXTRA.equalsIgnoreCase(extra)){ + this.notNull = false; + } + this.remark = remark; + this.listShow = true; + this.formShow = true; + } +} diff --git a/eladmin-generator/src/main/java/me/zhengjie/domain/GenConfig.java b/eladmin-generator/src/main/java/me/zhengjie/domain/GenConfig.java new file mode 100644 index 0000000..a2d6706 --- /dev/null +++ b/eladmin-generator/src/main/java/me/zhengjie/domain/GenConfig.java @@ -0,0 +1,78 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.domain; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import java.io.Serializable; + +/** + * 代码生成配置 + * @author Zheng Jie + * @date 2019-01-03 + */ +@Getter +@Setter +@Entity +@NoArgsConstructor +@Table(name = "code_gen_config") +public class GenConfig implements Serializable { + + public GenConfig(String tableName) { + this.tableName = tableName; + } + + @Id + @Column(name = "config_id") + @ApiModelProperty(value = "ID", hidden = true) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank + @ApiModelProperty(value = "表名") + private String tableName; + + @ApiModelProperty(value = "接口名称") + private String apiAlias; + + @NotBlank + @ApiModelProperty(value = "包路径") + private String pack; + + @NotBlank + @ApiModelProperty(value = "模块名") + private String moduleName; + + @NotBlank + @ApiModelProperty(value = "前端文件路径") + private String path; + + @ApiModelProperty(value = "前端文件路径") + private String apiPath; + + @ApiModelProperty(value = "作者") + private String author; + + @ApiModelProperty(value = "表前缀") + private String prefix; + + @ApiModelProperty(value = "是否覆盖") + private Boolean cover = false; +} diff --git a/eladmin-generator/src/main/java/me/zhengjie/domain/vo/TableInfo.java b/eladmin-generator/src/main/java/me/zhengjie/domain/vo/TableInfo.java new file mode 100644 index 0000000..1d3967b --- /dev/null +++ b/eladmin-generator/src/main/java/me/zhengjie/domain/vo/TableInfo.java @@ -0,0 +1,48 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 表的数据信息 + * @author Zheng Jie + * @date 2019-01-02 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TableInfo { + + /** 表名称 */ + private Object tableName; + + /** 创建日期 */ + private Object createTime; + + /** 数据库引擎 */ + private Object engine; + + /** 编码集 */ + private Object coding; + + /** 备注 */ + private Object remark; + + +} diff --git a/eladmin-generator/src/main/java/me/zhengjie/repository/ColumnInfoRepository.java b/eladmin-generator/src/main/java/me/zhengjie/repository/ColumnInfoRepository.java new file mode 100644 index 0000000..4638be2 --- /dev/null +++ b/eladmin-generator/src/main/java/me/zhengjie/repository/ColumnInfoRepository.java @@ -0,0 +1,34 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.repository; + +import me.zhengjie.domain.ColumnInfo; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +/** + * @author Zheng Jie + * @date 2019-01-14 + */ +public interface ColumnInfoRepository extends JpaRepository { + + /** + * 查询表信息 + * @param tableName 表格名 + * @return 表信息 + */ + List findByTableNameOrderByIdAsc(String tableName); +} diff --git a/eladmin-generator/src/main/java/me/zhengjie/repository/GenConfigRepository.java b/eladmin-generator/src/main/java/me/zhengjie/repository/GenConfigRepository.java new file mode 100644 index 0000000..18c9a0c --- /dev/null +++ b/eladmin-generator/src/main/java/me/zhengjie/repository/GenConfigRepository.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.repository; + +import me.zhengjie.domain.GenConfig; +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * @author Zheng Jie + * @date 2019-01-14 + */ +public interface GenConfigRepository extends JpaRepository { + + /** + * 查询表配置 + * @param tableName 表名 + * @return / + */ + GenConfig findByTableName(String tableName); +} diff --git a/eladmin-generator/src/main/java/me/zhengjie/rest/GenConfigController.java b/eladmin-generator/src/main/java/me/zhengjie/rest/GenConfigController.java new file mode 100644 index 0000000..d4cbec9 --- /dev/null +++ b/eladmin-generator/src/main/java/me/zhengjie/rest/GenConfigController.java @@ -0,0 +1,51 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import me.zhengjie.domain.GenConfig; +import me.zhengjie.service.GenConfigService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + * @author Zheng Jie + * @date 2019-01-14 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/genConfig") +@Api(tags = "系统:代码生成器配置管理") +public class GenConfigController { + + private final GenConfigService genConfigService; + + @ApiOperation("查询") + @GetMapping(value = "/{tableName}") + public ResponseEntity queryGenConfig(@PathVariable String tableName){ + return new ResponseEntity<>(genConfigService.find(tableName), HttpStatus.OK); + } + + @PutMapping + @ApiOperation("修改") + public ResponseEntity updateGenConfig(@Validated @RequestBody GenConfig genConfig){ + return new ResponseEntity<>(genConfigService.update(genConfig.getTableName(), genConfig),HttpStatus.OK); + } +} diff --git a/eladmin-generator/src/main/java/me/zhengjie/rest/GeneratorController.java b/eladmin-generator/src/main/java/me/zhengjie/rest/GeneratorController.java new file mode 100644 index 0000000..4a1f1e4 --- /dev/null +++ b/eladmin-generator/src/main/java/me/zhengjie/rest/GeneratorController.java @@ -0,0 +1,109 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import me.zhengjie.domain.ColumnInfo; +import me.zhengjie.domain.vo.TableInfo; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.service.GenConfigService; +import me.zhengjie.service.GeneratorService; +import me.zhengjie.utils.PageResult; +import me.zhengjie.utils.PageUtil; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * @author Zheng Jie + * @date 2019-01-02 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/generator") +@Api(tags = "系统:代码生成管理") +public class GeneratorController { + + private final GeneratorService generatorService; + private final GenConfigService genConfigService; + + @Value("${generator.enabled}") + private Boolean generatorEnabled; + + @ApiOperation("查询数据库数据") + @GetMapping(value = "/tables/all") + public ResponseEntity queryAllTables(){ + return new ResponseEntity<>(generatorService.getTables(), HttpStatus.OK); + } + + @ApiOperation("查询数据库数据") + @GetMapping(value = "/tables") + public ResponseEntity> queryTables(@RequestParam(defaultValue = "") String name, + @RequestParam(defaultValue = "0")Integer page, + @RequestParam(defaultValue = "10")Integer size){ + int[] startEnd = PageUtil.transToStartEnd(page, size); + return new ResponseEntity<>(generatorService.getTables(name,startEnd), HttpStatus.OK); + } + + @ApiOperation("查询字段数据") + @GetMapping(value = "/columns") + public ResponseEntity> queryColumns(@RequestParam String tableName){ + List columnInfos = generatorService.getColumns(tableName); + return new ResponseEntity<>(PageUtil.toPage(columnInfos,columnInfos.size()), HttpStatus.OK); + } + + @ApiOperation("保存字段数据") + @PutMapping + public ResponseEntity saveColumn(@RequestBody List columnInfos){ + generatorService.save(columnInfos); + return new ResponseEntity<>(HttpStatus.OK); + } + + @ApiOperation("同步字段数据") + @PostMapping(value = "sync") + public ResponseEntity syncColumn(@RequestBody List tables){ + for (String table : tables) { + generatorService.sync(generatorService.getColumns(table), generatorService.query(table)); + } + return new ResponseEntity<>(HttpStatus.OK); + } + + @ApiOperation("生成代码") + @PostMapping(value = "/{tableName}/{type}") + public ResponseEntity generatorCode(@PathVariable String tableName, @PathVariable Integer type, HttpServletRequest request, HttpServletResponse response){ + if(!generatorEnabled && type == 0){ + throw new BadRequestException("此环境不允许生成代码,请选择预览或者下载查看!"); + } + switch (type){ + // 生成代码 + case 0: generatorService.generator(genConfigService.find(tableName), generatorService.getColumns(tableName)); + break; + // 预览 + case 1: return generatorService.preview(genConfigService.find(tableName), generatorService.getColumns(tableName)); + // 打包 + case 2: generatorService.download(genConfigService.find(tableName), generatorService.getColumns(tableName), request, response); + break; + default: throw new BadRequestException("没有这个选项"); + } + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/eladmin-generator/src/main/java/me/zhengjie/service/GenConfigService.java b/eladmin-generator/src/main/java/me/zhengjie/service/GenConfigService.java new file mode 100644 index 0000000..b5711f4 --- /dev/null +++ b/eladmin-generator/src/main/java/me/zhengjie/service/GenConfigService.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service; + +import me.zhengjie.domain.GenConfig; + +/** + * @author Zheng Jie + * @date 2019-01-14 + */ +public interface GenConfigService { + + /** + * 查询表配置 + * @param tableName 表名 + * @return 表配置 + */ + GenConfig find(String tableName); + + /** + * 更新表配置 + * @param tableName 表名 + * @param genConfig 表配置 + * @return 表配置 + */ + GenConfig update(String tableName, GenConfig genConfig); +} diff --git a/eladmin-generator/src/main/java/me/zhengjie/service/GeneratorService.java b/eladmin-generator/src/main/java/me/zhengjie/service/GeneratorService.java new file mode 100644 index 0000000..b9c2a2f --- /dev/null +++ b/eladmin-generator/src/main/java/me/zhengjie/service/GeneratorService.java @@ -0,0 +1,98 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service; + +import me.zhengjie.domain.GenConfig; +import me.zhengjie.domain.ColumnInfo; +import me.zhengjie.domain.vo.TableInfo; +import me.zhengjie.utils.PageResult; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.Async; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * @author Zheng Jie + * @date 2019-01-02 + */ +public interface GeneratorService { + + /** + * 查询数据库元数据 + * @param name 表名 + * @param startEnd 分页参数 + * @return / + */ + PageResult getTables(String name, int[] startEnd); + + /** + * 得到数据表的元数据 + * @param name 表名 + * @return / + */ + List getColumns(String name); + + /** + * 同步表数据 + * @param columnInfos / + * @param columnInfoList / + */ + void sync(List columnInfos, List columnInfoList); + + /** + * 保持数据 + * @param columnInfos / + */ + void save(List columnInfos); + + /** + * 获取所有table + * @return / + */ + Object getTables(); + + /** + * 代码生成 + * @param genConfig 配置信息 + * @param columns 字段信息 + */ + void generator(GenConfig genConfig, List columns); + + /** + * 预览 + * @param genConfig 配置信息 + * @param columns 字段信息 + * @return / + */ + ResponseEntity preview(GenConfig genConfig, List columns); + + /** + * 打包下载 + * @param genConfig 配置信息 + * @param columns 字段信息 + * @param request / + * @param response / + */ + void download(GenConfig genConfig, List columns, HttpServletRequest request, HttpServletResponse response); + + /** + * 查询数据库的表字段数据数据 + * @param table / + * @return / + */ + List query(String table); +} diff --git a/eladmin-generator/src/main/java/me/zhengjie/service/impl/GenConfigServiceImpl.java b/eladmin-generator/src/main/java/me/zhengjie/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000..bc2d061 --- /dev/null +++ b/eladmin-generator/src/main/java/me/zhengjie/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,67 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service.impl; + +import lombok.RequiredArgsConstructor; +import me.zhengjie.domain.GenConfig; +import me.zhengjie.repository.GenConfigRepository; +import me.zhengjie.service.GenConfigService; +import me.zhengjie.utils.StringUtils; +import org.springframework.stereotype.Service; +import java.io.File; + +/** + * @author Zheng Jie + * @date 2019-01-14 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl implements GenConfigService { + + private final GenConfigRepository genConfigRepository; + + @Override + public GenConfig find(String tableName) { + GenConfig genConfig = genConfigRepository.findByTableName(tableName); + if(genConfig == null){ + return new GenConfig(tableName); + } + return genConfig; + } + + @Override + public GenConfig update(String tableName, GenConfig genConfig) { + String separator = File.separator; + String[] paths; + String symbol = "\\"; + if (symbol.equals(separator)) { + paths = genConfig.getPath().split("\\\\"); + } else { + paths = genConfig.getPath().split(File.separator); + } + StringBuilder api = new StringBuilder(); + for (String path : paths) { + api.append(path); + api.append(separator); + if ("src".equals(path)) { + api.append("api"); + break; + } + } + genConfig.setApiPath(api.toString()); + return genConfigRepository.save(genConfig); + } +} diff --git a/eladmin-generator/src/main/java/me/zhengjie/service/impl/GeneratorServiceImpl.java b/eladmin-generator/src/main/java/me/zhengjie/service/impl/GeneratorServiceImpl.java new file mode 100644 index 0000000..604cf2d --- /dev/null +++ b/eladmin-generator/src/main/java/me/zhengjie/service/impl/GeneratorServiceImpl.java @@ -0,0 +1,204 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ZipUtil; +import lombok.RequiredArgsConstructor; +import me.zhengjie.domain.GenConfig; +import me.zhengjie.domain.ColumnInfo; +import me.zhengjie.domain.vo.TableInfo; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.repository.ColumnInfoRepository; +import me.zhengjie.service.GeneratorService; +import me.zhengjie.utils.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author Zheng Jie + * @date 2019-01-02 + */ +@Service +@RequiredArgsConstructor +public class GeneratorServiceImpl implements GeneratorService { + private static final Logger log = LoggerFactory.getLogger(GeneratorServiceImpl.class); + @PersistenceContext + private EntityManager em; + + private final ColumnInfoRepository columnInfoRepository; + + private final String CONFIG_MESSAGE = "请先配置生成器"; + @Override + public Object getTables() { + // 使用预编译防止sql注入 + String sql = "select table_name ,create_time , engine, table_collation, table_comment from information_schema.tables " + + "where table_schema = (select database()) " + + "order by create_time desc"; + Query query = em.createNativeQuery(sql); + return query.getResultList(); + } + + @Override + public PageResult getTables(String name, int[] startEnd) { + // 使用预编译防止sql注入 + String sql = "select table_name ,create_time , engine, table_collation, table_comment from information_schema.tables " + + "where table_schema = (select database()) " + + "and table_name like :table order by create_time desc"; + Query query = em.createNativeQuery(sql); + query.setFirstResult(startEnd[0]); + query.setMaxResults(startEnd[1] - startEnd[0]); + query.setParameter("table", StringUtils.isNotBlank(name) ? ("%" + name + "%") : "%%"); + List result = query.getResultList(); + List tableInfos = new ArrayList<>(); + for (Object obj : result) { + Object[] arr = (Object[]) obj; + tableInfos.add(new TableInfo(arr[0], arr[1], arr[2], arr[3], ObjectUtil.isNotEmpty(arr[4]) ? arr[4] : "-")); + } + String countSql = "select count(1) from information_schema.tables " + + "where table_schema = (select database()) and table_name like :table"; + Query queryCount = em.createNativeQuery(countSql); + queryCount.setParameter("table", StringUtils.isNotBlank(name) ? ("%" + name + "%") : "%%"); + BigInteger totalElements = (BigInteger) queryCount.getSingleResult(); + return PageUtil.toPage(tableInfos, totalElements.longValue()); + } + + @Override + public List getColumns(String tableName) { + List columnInfos = columnInfoRepository.findByTableNameOrderByIdAsc(tableName); + if (CollectionUtil.isNotEmpty(columnInfos)) { + return columnInfos; + } else { + columnInfos = query(tableName); + return columnInfoRepository.saveAll(columnInfos); + } + } + + @Override + public List query(String tableName) { + // 使用预编译防止sql注入 + String sql = "select column_name, is_nullable, data_type, column_comment, column_key, extra from information_schema.columns " + + "where table_name = ? and table_schema = (select database()) order by ordinal_position"; + Query query = em.createNativeQuery(sql); + query.setParameter(1, tableName); + List result = query.getResultList(); + List columnInfos = new ArrayList<>(); + for (Object obj : result) { + Object[] arr = (Object[]) obj; + columnInfos.add( + new ColumnInfo( + tableName, + arr[0].toString(), + "NO".equals(arr[1]), + arr[2].toString(), + ObjectUtil.isNotNull(arr[3]) ? arr[3].toString() : null, + ObjectUtil.isNotNull(arr[4]) ? arr[4].toString() : null, + ObjectUtil.isNotNull(arr[5]) ? arr[5].toString() : null) + ); + } + return columnInfos; + } + + @Override + public void sync(List columnInfos, List columnInfoList) { + // 第一种情况,数据库类字段改变或者新增字段 + for (ColumnInfo columnInfo : columnInfoList) { + // 根据字段名称查找 + List columns = columnInfos.stream().filter(c -> c.getColumnName().equals(columnInfo.getColumnName())).collect(Collectors.toList()); + // 如果能找到,就修改部分可能被字段 + if (CollectionUtil.isNotEmpty(columns)) { + ColumnInfo column = columns.get(0); + column.setColumnType(columnInfo.getColumnType()); + column.setExtra(columnInfo.getExtra()); + column.setKeyType(columnInfo.getKeyType()); + if (StringUtils.isBlank(column.getRemark())) { + column.setRemark(columnInfo.getRemark()); + } + columnInfoRepository.save(column); + } else { + // 如果找不到,则保存新字段信息 + columnInfoRepository.save(columnInfo); + } + } + // 第二种情况,数据库字段删除了 + for (ColumnInfo columnInfo : columnInfos) { + // 根据字段名称查找 + List columns = columnInfoList.stream().filter(c -> c.getColumnName().equals(columnInfo.getColumnName())).collect(Collectors.toList()); + // 如果找不到,就代表字段被删除了,则需要删除该字段 + if (CollectionUtil.isEmpty(columns)) { + columnInfoRepository.delete(columnInfo); + } + } + } + + @Override + public void save(List columnInfos) { + columnInfoRepository.saveAll(columnInfos); + } + + @Override + public void generator(GenConfig genConfig, List columns) { + if (genConfig.getId() == null) { + throw new BadRequestException(CONFIG_MESSAGE); + } + try { + GenUtil.generatorCode(columns, genConfig); + } catch (IOException e) { + log.error(e.getMessage(), e); + throw new BadRequestException("生成失败,请手动处理已生成的文件"); + } + } + + @Override + public ResponseEntity preview(GenConfig genConfig, List columns) { + if (genConfig.getId() == null) { + throw new BadRequestException(CONFIG_MESSAGE); + } + List> genList = GenUtil.preview(columns, genConfig); + return new ResponseEntity<>(genList, HttpStatus.OK); + } + + @Override + public void download(GenConfig genConfig, List columns, HttpServletRequest request, HttpServletResponse response) { + if (genConfig.getId() == null) { + throw new BadRequestException(CONFIG_MESSAGE); + } + try { + File file = new File(GenUtil.download(columns, genConfig)); + String zipPath = file.getPath() + ".zip"; + ZipUtil.zip(file.getPath(), zipPath); + FileUtil.downloadFile(request, response, new File(zipPath), true); + } catch (IOException e) { + throw new BadRequestException("打包失败"); + } + } +} diff --git a/eladmin-generator/src/main/java/me/zhengjie/utils/ColUtil.java b/eladmin-generator/src/main/java/me/zhengjie/utils/ColUtil.java new file mode 100644 index 0000000..b5fcd6b --- /dev/null +++ b/eladmin-generator/src/main/java/me/zhengjie/utils/ColUtil.java @@ -0,0 +1,54 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.utils; + +import org.apache.commons.configuration.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * sql字段转java + * + * @author Zheng Jie + * @date 2019-01-03 + */ +public class ColUtil { + private static final Logger log = LoggerFactory.getLogger(ColUtil.class); + + /** + * 转换mysql数据类型为java数据类型 + * + * @param type 数据库字段类型 + * @return String + */ + static String cloToJava(String type) { + Configuration config = getConfig(); + assert config != null; + return config.getString(type, "unknowType"); + } + + /** + * 获取配置信息 + */ + public static PropertiesConfiguration getConfig() { + try { + return new PropertiesConfiguration("generator.properties"); + } catch (ConfigurationException e) { + log.error(e.getMessage(), e); + } + return null; + } +} diff --git a/eladmin-generator/src/main/java/me/zhengjie/utils/GenUtil.java b/eladmin-generator/src/main/java/me/zhengjie/utils/GenUtil.java new file mode 100644 index 0000000..8c8a95a --- /dev/null +++ b/eladmin-generator/src/main/java/me/zhengjie/utils/GenUtil.java @@ -0,0 +1,422 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.utils; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.*; +import lombok.extern.slf4j.Slf4j; +import me.zhengjie.domain.GenConfig; +import me.zhengjie.domain.ColumnInfo; +import org.springframework.util.ObjectUtils; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.time.LocalDate; +import java.util.*; + +import static me.zhengjie.utils.FileUtil.SYS_TEM_DIR; + +/** + * 代码生成 + * + * @author Zheng Jie + * @date 2019-01-02 + */ +@Slf4j +@SuppressWarnings({"unchecked", "all"}) +public class GenUtil { + + private static final String TIMESTAMP = "Timestamp"; + + private static final String BIGDECIMAL = "BigDecimal"; + + public static final String PK = "PRI"; + + public static final String EXTRA = "auto_increment"; + + /** + * 获取后端代码模板名称 + * + * @return List + */ + private static List getAdminTemplateNames() { + List templateNames = new ArrayList<>(); + templateNames.add("Entity"); + templateNames.add("Dto"); + templateNames.add("Mapper"); + templateNames.add("Controller"); + templateNames.add("QueryCriteria"); + templateNames.add("Service"); + templateNames.add("ServiceImpl"); + templateNames.add("Repository"); + return templateNames; + } + + /** + * 获取前端代码模板名称 + * + * @return List + */ + private static List getFrontTemplateNames() { + List templateNames = new ArrayList<>(); + templateNames.add("index"); + templateNames.add("api"); + return templateNames; + } + + public static List> preview(List columns, GenConfig genConfig) { + Map genMap = getGenMap(columns, genConfig); + List> genList = new ArrayList<>(); + // 获取后端模版 + List templates = getAdminTemplateNames(); + TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("template", TemplateConfig.ResourceMode.CLASSPATH)); + for (String templateName : templates) { + Map map = new HashMap<>(1); + Template template = engine.getTemplate("admin/" + templateName + ".ftl"); + map.put("content", template.render(genMap)); + map.put("name", templateName); + genList.add(map); + } + // 获取前端模版 + templates = getFrontTemplateNames(); + for (String templateName : templates) { + Map map = new HashMap<>(1); + Template template = engine.getTemplate("front/" + templateName + ".ftl"); + map.put(templateName, template.render(genMap)); + map.put("content", template.render(genMap)); + map.put("name", templateName); + genList.add(map); + } + return genList; + } + + public static String download(List columns, GenConfig genConfig) throws IOException { + // 拼接的路径:/tmpeladmin-gen-temp/,这个路径在Linux下需要root用户才有权限创建,非root用户会权限错误而失败,更改为: /tmp/eladmin-gen-temp/ + // String tempPath =SYS_TEM_DIR + "eladmin-gen-temp" + File.separator + genConfig.getTableName() + File.separator; + String tempPath = SYS_TEM_DIR + "eladmin-gen-temp" + File.separator + genConfig.getTableName() + File.separator; + Map genMap = getGenMap(columns, genConfig); + TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("template", TemplateConfig.ResourceMode.CLASSPATH)); + // 生成后端代码 + List templates = getAdminTemplateNames(); + for (String templateName : templates) { + Template template = engine.getTemplate("admin/" + templateName + ".ftl"); + String filePath = getAdminFilePath(templateName, genConfig, genMap.get("className").toString(), tempPath + "eladmin" + File.separator); + assert filePath != null; + File file = new File(filePath); + // 如果非覆盖生成 + if (!genConfig.getCover() && FileUtil.exist(file)) { + continue; + } + // 生成代码 + genFile(file, template, genMap); + } + // 生成前端代码 + templates = getFrontTemplateNames(); + for (String templateName : templates) { + Template template = engine.getTemplate("front/" + templateName + ".ftl"); + String path = tempPath + "eladmin-web" + File.separator; + String apiPath = path + "src" + File.separator + "api" + File.separator; + String srcPath = path + "src" + File.separator + "views" + File.separator + genMap.get("changeClassName").toString() + File.separator; + String filePath = getFrontFilePath(templateName, apiPath, srcPath, genMap.get("changeClassName").toString()); + assert filePath != null; + File file = new File(filePath); + // 如果非覆盖生成 + if (!genConfig.getCover() && FileUtil.exist(file)) { + continue; + } + // 生成代码 + genFile(file, template, genMap); + } + return tempPath; + } + + public static void generatorCode(List columnInfos, GenConfig genConfig) throws IOException { + Map genMap = getGenMap(columnInfos, genConfig); + TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("template", TemplateConfig.ResourceMode.CLASSPATH)); + // 生成后端代码 + List templates = getAdminTemplateNames(); + for (String templateName : templates) { + Template template = engine.getTemplate("admin/" + templateName + ".ftl"); + String rootPath = System.getProperty("user.dir"); + String filePath = getAdminFilePath(templateName, genConfig, genMap.get("className").toString(), rootPath); + + assert filePath != null; + File file = new File(filePath); + + // 如果非覆盖生成 + if (!genConfig.getCover() && FileUtil.exist(file)) { + continue; + } + // 生成代码 + genFile(file, template, genMap); + } + + // 生成前端代码 + templates = getFrontTemplateNames(); + for (String templateName : templates) { + Template template = engine.getTemplate("front/" + templateName + ".ftl"); + String filePath = getFrontFilePath(templateName, genConfig.getApiPath(), genConfig.getPath(), genMap.get("changeClassName").toString()); + + assert filePath != null; + File file = new File(filePath); + + // 如果非覆盖生成 + if (!genConfig.getCover() && FileUtil.exist(file)) { + continue; + } + // 生成代码 + genFile(file, template, genMap); + } + } + + // 获取模版数据 + private static Map getGenMap(List columnInfos, GenConfig genConfig) { + // 存储模版字段数据 + Map genMap = new HashMap<>(16); + // 接口别名 + genMap.put("apiAlias", genConfig.getApiAlias()); + // 包名称 + genMap.put("package", genConfig.getPack()); + // 模块名称 + genMap.put("moduleName", genConfig.getModuleName()); + // 作者 + genMap.put("author", genConfig.getAuthor()); + // 创建日期 + genMap.put("date", LocalDate.now().toString()); + // 表名 + genMap.put("tableName", genConfig.getTableName()); + // 大写开头的类名 + String className = StringUtils.toCapitalizeCamelCase(genConfig.getTableName()); + // 小写开头的类名 + String changeClassName = StringUtils.toCamelCase(genConfig.getTableName()); + // 判断是否去除表前缀 + if (StringUtils.isNotEmpty(genConfig.getPrefix())) { + className = StringUtils.toCapitalizeCamelCase(StrUtil.removePrefix(genConfig.getTableName(), genConfig.getPrefix())); + changeClassName = StringUtils.toCamelCase(StrUtil.removePrefix(genConfig.getTableName(), genConfig.getPrefix())); + changeClassName = StringUtils.uncapitalize(changeClassName); + } + // 保存类名 + genMap.put("className", className); + // 保存小写开头的类名 + genMap.put("changeClassName", changeClassName); + // 存在 Timestamp 字段 + genMap.put("hasTimestamp", false); + // 查询类中存在 Timestamp 字段 + genMap.put("queryHasTimestamp", false); + // 存在 BigDecimal 字段 + genMap.put("hasBigDecimal", false); + // 查询类中存在 BigDecimal 字段 + genMap.put("queryHasBigDecimal", false); + // 是否需要创建查询 + genMap.put("hasQuery", false); + // 自增主键 + genMap.put("auto", false); + // 存在字典 + genMap.put("hasDict", false); + // 存在日期注解 + genMap.put("hasDateAnnotation", false); + // 保存字段信息 + List> columns = new ArrayList<>(); + // 保存查询字段的信息 + List> queryColumns = new ArrayList<>(); + // 存储字典信息 + List dicts = new ArrayList<>(); + // 存储 between 信息 + List> betweens = new ArrayList<>(); + // 存储不为空的字段信息 + List> isNotNullColumns = new ArrayList<>(); + + for (ColumnInfo column : columnInfos) { + Map listMap = new HashMap<>(16); + // 字段描述 + listMap.put("remark", column.getRemark()); + // 字段类型 + listMap.put("columnKey", column.getKeyType()); + // 主键类型 + String colType = ColUtil.cloToJava(column.getColumnType()); + // 小写开头的字段名 + String changeColumnName = StringUtils.toCamelCase(column.getColumnName()); + // 大写开头的字段名 + String capitalColumnName = StringUtils.toCapitalizeCamelCase(column.getColumnName()); + if (PK.equals(column.getKeyType())) { + // 存储主键类型 + genMap.put("pkColumnType", colType); + // 存储小写开头的字段名 + genMap.put("pkChangeColName", changeColumnName); + // 存储大写开头的字段名 + genMap.put("pkCapitalColName", capitalColumnName); + } + // 是否存在 Timestamp 类型的字段 + if (TIMESTAMP.equals(colType)) { + genMap.put("hasTimestamp", true); + } + // 是否存在 BigDecimal 类型的字段 + if (BIGDECIMAL.equals(colType)) { + genMap.put("hasBigDecimal", true); + } + // 主键是否自增 + if (EXTRA.equals(column.getExtra())) { + genMap.put("auto", true); + } + // 主键存在字典 + if (StringUtils.isNotBlank(column.getDictName())) { + genMap.put("hasDict", true); + if(!dicts.contains(column.getDictName())) + dicts.add(column.getDictName()); + } + + // 存储字段类型 + listMap.put("columnType", colType); + // 存储字原始段名称 + listMap.put("columnName", column.getColumnName()); + // 不为空 + listMap.put("istNotNull", column.getNotNull()); + // 字段列表显示 + listMap.put("columnShow", column.getListShow()); + // 表单显示 + listMap.put("formShow", column.getFormShow()); + // 表单组件类型 + listMap.put("formType", StringUtils.isNotBlank(column.getFormType()) ? column.getFormType() : "Input"); + // 小写开头的字段名称 + listMap.put("changeColumnName", changeColumnName); + //大写开头的字段名称 + listMap.put("capitalColumnName", capitalColumnName); + // 字典名称 + listMap.put("dictName", column.getDictName()); + // 日期注解 + listMap.put("dateAnnotation", column.getDateAnnotation()); + if (StringUtils.isNotBlank(column.getDateAnnotation())) { + genMap.put("hasDateAnnotation", true); + } + // 添加非空字段信息 + if (column.getNotNull()) { + isNotNullColumns.add(listMap); + } + // 判断是否有查询,如有则把查询的字段set进columnQuery + if (!StringUtils.isBlank(column.getQueryType())) { + // 查询类型 + listMap.put("queryType", column.getQueryType()); + // 是否存在查询 + genMap.put("hasQuery", true); + if (TIMESTAMP.equals(colType)) { + // 查询中存储 Timestamp 类型 + genMap.put("queryHasTimestamp", true); + } + if (BIGDECIMAL.equals(colType)) { + // 查询中存储 BigDecimal 类型 + genMap.put("queryHasBigDecimal", true); + } + if ("between".equalsIgnoreCase(column.getQueryType())) { + betweens.add(listMap); + } else { + // 添加到查询列表中 + queryColumns.add(listMap); + } + } + // 添加到字段列表中 + columns.add(listMap); + } + // 保存字段列表 + genMap.put("columns", columns); + // 保存查询列表 + genMap.put("queryColumns", queryColumns); + // 保存字段列表 + genMap.put("dicts", dicts); + // 保存查询列表 + genMap.put("betweens", betweens); + // 保存非空字段信息 + genMap.put("isNotNullColumns", isNotNullColumns); + return genMap; + } + + /** + * 定义后端文件路径以及名称 + */ + private static String getAdminFilePath(String templateName, GenConfig genConfig, String className, String rootPath) { + String projectPath = rootPath + File.separator + genConfig.getModuleName(); + String packagePath = projectPath + File.separator + "src" + File.separator + "main" + File.separator + "java" + File.separator; + if (!ObjectUtils.isEmpty(genConfig.getPack())) { + packagePath += genConfig.getPack().replace(".", File.separator) + File.separator; + } + + if ("Entity".equals(templateName)) { + return packagePath + "domain" + File.separator + className + ".java"; + } + + if ("Controller".equals(templateName)) { + return packagePath + "rest" + File.separator + className + "Controller.java"; + } + + if ("Service".equals(templateName)) { + return packagePath + "service" + File.separator + className + "Service.java"; + } + + if ("ServiceImpl".equals(templateName)) { + return packagePath + "service" + File.separator + "impl" + File.separator + className + "ServiceImpl.java"; + } + + if ("Dto".equals(templateName)) { + return packagePath + "service" + File.separator + "dto" + File.separator + className + "Dto.java"; + } + + if ("QueryCriteria".equals(templateName)) { + return packagePath + "service" + File.separator + "dto" + File.separator + className + "QueryCriteria.java"; + } + + if ("Mapper".equals(templateName)) { + return packagePath + "service" + File.separator + "mapstruct" + File.separator + className + "Mapper.java"; + } + + if ("Repository".equals(templateName)) { + return packagePath + "repository" + File.separator + className + "Repository.java"; + } + + return null; + } + + /** + * 定义前端文件路径以及名称 + */ + private static String getFrontFilePath(String templateName, String apiPath, String path, String apiName) { + + if ("api".equals(templateName)) { + return apiPath + File.separator + apiName + ".js"; + } + + if ("index".equals(templateName)) { + return path + File.separator + "index.vue"; + } + + return null; + } + + private static void genFile(File file, Template template, Map map) throws IOException { + // 生成目标文件 + Writer writer = null; + try { + FileUtil.touch(file); + writer = new FileWriter(file); + template.render(map, writer); + } catch (TemplateException | IOException e) { + throw new RuntimeException(e); + } finally { + assert writer != null; + writer.close(); + } + } +} diff --git a/eladmin-generator/src/main/resources/generator.properties b/eladmin-generator/src/main/resources/generator.properties new file mode 100644 index 0000000..e64d060 --- /dev/null +++ b/eladmin-generator/src/main/resources/generator.properties @@ -0,0 +1,27 @@ +# Database type to Java type +tinyint=Integer +smallint=Integer +mediumint=Integer +int=Integer +integer=Integer + +bigint=Long + +float=Float + +double=Double + +decimal=BigDecimal + +bit=Boolean + +char=String +varchar=String +tinytext=String +text=String +mediumtext=String +longtext=String + +date=Timestamp +datetime=Timestamp +timestamp=Timestamp \ No newline at end of file diff --git a/eladmin-generator/src/main/resources/template/admin/Controller.ftl b/eladmin-generator/src/main/resources/template/admin/Controller.ftl new file mode 100644 index 0000000..686d974 --- /dev/null +++ b/eladmin-generator/src/main/resources/template/admin/Controller.ftl @@ -0,0 +1,89 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package ${package}.rest; + +import me.zhengjie.annotation.Log; +import ${package}.domain.${className}; +import ${package}.service.${className}Service; +import ${package}.service.dto.${className}QueryCriteria; +import org.springframework.data.domain.Pageable; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import io.swagger.annotations.*; +import java.io.IOException; +import javax.servlet.http.HttpServletResponse; +import me.zhengjie.utils.PageResult; +import ${package}.service.dto.${className}Dto; + +/** +* @website https://eladmin.vip +* @author ${author} +**/ +@RestController +@RequiredArgsConstructor +@Api(tags = "${apiAlias}管理") +@RequestMapping("/api/${changeClassName}") +public class ${className}Controller { + + private final ${className}Service ${changeClassName}Service; + + @Log("导出数据") + @ApiOperation("导出数据") + @GetMapping(value = "/download") + @PreAuthorize("@el.check('${changeClassName}:list')") + public void export${className}(HttpServletResponse response, ${className}QueryCriteria criteria) throws IOException { + ${changeClassName}Service.download(${changeClassName}Service.queryAll(criteria), response); + } + + @GetMapping + @Log("查询${apiAlias}") + @ApiOperation("查询${apiAlias}") + @PreAuthorize("@el.check('${changeClassName}:list')") + public ResponseEntity> query${className}(${className}QueryCriteria criteria, Pageable pageable){ + return new ResponseEntity<>(${changeClassName}Service.queryAll(criteria,pageable),HttpStatus.OK); + } + + @PostMapping + @Log("新增${apiAlias}") + @ApiOperation("新增${apiAlias}") + @PreAuthorize("@el.check('${changeClassName}:add')") + public ResponseEntity create${className}(@Validated @RequestBody ${className} resources){ + ${changeClassName}Service.create(resources); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @PutMapping + @Log("修改${apiAlias}") + @ApiOperation("修改${apiAlias}") + @PreAuthorize("@el.check('${changeClassName}:edit')") + public ResponseEntity update${className}(@Validated @RequestBody ${className} resources){ + ${changeClassName}Service.update(resources); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @DeleteMapping + @Log("删除${apiAlias}") + @ApiOperation("删除${apiAlias}") + @PreAuthorize("@el.check('${changeClassName}:del')") + public ResponseEntity delete${className}(@RequestBody ${pkColumnType}[] ids) { + ${changeClassName}Service.deleteAll(ids); + return new ResponseEntity<>(HttpStatus.OK); + } +} \ No newline at end of file diff --git a/eladmin-generator/src/main/resources/template/admin/Dto.ftl b/eladmin-generator/src/main/resources/template/admin/Dto.ftl new file mode 100644 index 0000000..1b010cc --- /dev/null +++ b/eladmin-generator/src/main/resources/template/admin/Dto.ftl @@ -0,0 +1,53 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package ${package}.service.dto; + +import lombok.Data; +<#if hasTimestamp> +import java.sql.Timestamp; + +<#if hasBigDecimal> +import java.math.BigDecimal; + +import java.io.Serializable; +<#if !auto && pkColumnType = 'Long'> +import com.alibaba.fastjson.annotation.JSONField; +import com.alibaba.fastjson.serializer.ToStringSerializer; + + +/** +* @website https://eladmin.vip +* @description / +* @author ${author} +**/ +@Data +public class ${className}Dto implements Serializable { +<#if columns??> + <#list columns as column> + + <#if column.remark != ''> + /** ${column.remark} */ + + <#if column.columnKey = 'PRI'> + <#if !auto && pkColumnType = 'Long'> + /** 防止精度丢失 */ + @JSONField(serializeUsing = ToStringSerializer.class) + + + private ${column.columnType} ${column.changeColumnName}; + + +} \ No newline at end of file diff --git a/eladmin-generator/src/main/resources/template/admin/Entity.ftl b/eladmin-generator/src/main/resources/template/admin/Entity.ftl new file mode 100644 index 0000000..fa96624 --- /dev/null +++ b/eladmin-generator/src/main/resources/template/admin/Entity.ftl @@ -0,0 +1,84 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package ${package}.domain; + +import lombok.Data; +import cn.hutool.core.bean.BeanUtil; +import io.swagger.annotations.ApiModelProperty; +import cn.hutool.core.bean.copier.CopyOptions; +import javax.persistence.*; +<#if isNotNullColumns??> +import javax.validation.constraints.*; + +<#if hasDateAnnotation> +import javax.persistence.Entity; +import javax.persistence.Table; +import org.hibernate.annotations.*; + +<#if hasTimestamp> +import java.sql.Timestamp; + +<#if hasBigDecimal> +import java.math.BigDecimal; + +import java.io.Serializable; + +/** +* @website https://eladmin.vip +* @description / +* @author ${author} +**/ +@Entity +@Data +@Table(name="${tableName}") +public class ${className} implements Serializable { +<#if columns??> + <#list columns as column> + + <#if column.columnKey = 'PRI'> + @Id + <#if auto> + @GeneratedValue(strategy = GenerationType.IDENTITY) + + + @Column(name = "`${column.columnName}`"<#if column.columnKey = 'UNI'>,unique = true<#if column.istNotNull && column.columnKey != 'PRI'>,nullable = false) + <#if column.istNotNull && column.columnKey != 'PRI'> + <#if column.columnType = 'String'> + @NotBlank + <#else> + @NotNull + + + <#if (column.dateAnnotation)?? && column.dateAnnotation != ''> + <#if column.dateAnnotation = 'CreationTimestamp'> + @CreationTimestamp + <#else> + @UpdateTimestamp + + + <#if column.remark != ''> + @ApiModelProperty(value = "${column.remark}") + <#else> + @ApiModelProperty(value = "${column.changeColumnName}") + + private ${column.columnType} ${column.changeColumnName}; + + + + public void copy(${className} source){ + BeanUtil.copyProperties(source,this, CopyOptions.create().setIgnoreNullValue(true)); + } +} diff --git a/eladmin-generator/src/main/resources/template/admin/Mapper.ftl b/eladmin-generator/src/main/resources/template/admin/Mapper.ftl new file mode 100644 index 0000000..af04eca --- /dev/null +++ b/eladmin-generator/src/main/resources/template/admin/Mapper.ftl @@ -0,0 +1,31 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package ${package}.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import ${package}.domain.${className}; +import ${package}.service.dto.${className}Dto; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** +* @website https://eladmin.vip +* @author ${author} +**/ +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface ${className}Mapper extends BaseMapper<${className}Dto, ${className}> { + +} \ No newline at end of file diff --git a/eladmin-generator/src/main/resources/template/admin/QueryCriteria.ftl b/eladmin-generator/src/main/resources/template/admin/QueryCriteria.ftl new file mode 100644 index 0000000..1e9a989 --- /dev/null +++ b/eladmin-generator/src/main/resources/template/admin/QueryCriteria.ftl @@ -0,0 +1,80 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package ${package}.service.dto; + +import lombok.Data; +<#if queryHasTimestamp> +import java.sql.Timestamp; + +<#if queryHasBigDecimal> +import java.math.BigDecimal; + +<#if betweens??> +import java.util.List; + +<#if queryColumns??> +import me.zhengjie.annotation.Query; + + +/** +* @website https://eladmin.vip +* @author ${author} +**/ +@Data +public class ${className}QueryCriteria{ +<#if queryColumns??> + <#list queryColumns as column> + +<#if column.queryType = '='> + /** 精确 */ + @Query + private ${column.columnType} ${column.changeColumnName}; + +<#if column.queryType = 'Like'> + /** 模糊 */ + @Query(type = Query.Type.INNER_LIKE) + private ${column.columnType} ${column.changeColumnName}; + +<#if column.queryType = '!='> + /** 不等于 */ + @Query(type = Query.Type.NOT_EQUAL) + private ${column.columnType} ${column.changeColumnName}; + +<#if column.queryType = 'NotNull'> + /** 不为空 */ + @Query(type = Query.Type.NOT_NULL) + private ${column.columnType} ${column.changeColumnName}; + +<#if column.queryType = '>='> + /** 大于等于 */ + @Query(type = Query.Type.GREATER_THAN) + private ${column.columnType} ${column.changeColumnName}; + +<#if column.queryType = '<='> + /** 小于等于 */ + @Query(type = Query.Type.LESS_THAN) + private ${column.columnType} ${column.changeColumnName}; + + + +<#if betweens??> + <#list betweens as column> + /** BETWEEN */ + @Query(type = Query.Type.BETWEEN) + private List<${column.columnType}> ${column.changeColumnName}; + + +} \ No newline at end of file diff --git a/eladmin-generator/src/main/resources/template/admin/Repository.ftl b/eladmin-generator/src/main/resources/template/admin/Repository.ftl new file mode 100644 index 0000000..4e7398a --- /dev/null +++ b/eladmin-generator/src/main/resources/template/admin/Repository.ftl @@ -0,0 +1,39 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package ${package}.repository; + +import ${package}.domain.${className}; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** +* @website https://eladmin.vip +* @author ${author} +**/ +public interface ${className}Repository extends JpaRepository<${className}, ${pkColumnType}>, JpaSpecificationExecutor<${className}> { +<#if columns??> + <#list columns as column> + <#if column.columnKey = 'UNI'> + /** + * 根据 ${column.capitalColumnName} 查询 + * @param ${column.columnName} / + * @return / + */ + ${className} findBy${column.capitalColumnName}(${column.columnType} ${column.columnName}); + + + +} \ No newline at end of file diff --git a/eladmin-generator/src/main/resources/template/admin/Service.ftl b/eladmin-generator/src/main/resources/template/admin/Service.ftl new file mode 100644 index 0000000..83482ef --- /dev/null +++ b/eladmin-generator/src/main/resources/template/admin/Service.ftl @@ -0,0 +1,82 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package ${package}.service; + +import ${package}.domain.${className}; +import ${package}.service.dto.${className}Dto; +import ${package}.service.dto.${className}QueryCriteria; +import org.springframework.data.domain.Pageable; +import java.util.Map; +import java.util.List; +import java.io.IOException; +import javax.servlet.http.HttpServletResponse; +import me.zhengjie.utils.PageResult; + +/** +* @website https://eladmin.vip +* @description 服务接口 +* @author ${author} +**/ +public interface ${className}Service { + + /** + * 查询数据分页 + * @param criteria 条件 + * @param pageable 分页参数 + * @return Map + */ + PageResult<${className}Dto> queryAll(${className}QueryCriteria criteria, Pageable pageable); + + /** + * 查询所有数据不分页 + * @param criteria 条件参数 + * @return List<${className}Dto> + */ + List<${className}Dto> queryAll(${className}QueryCriteria criteria); + + /** + * 根据ID查询 + * @param ${pkChangeColName} ID + * @return ${className}Dto + */ + ${className}Dto findById(${pkColumnType} ${pkChangeColName}); + + /** + * 创建 + * @param resources / + */ + void create(${className} resources); + + /** + * 编辑 + * @param resources / + */ + void update(${className} resources); + + /** + * 多选删除 + * @param ids / + */ + void deleteAll(${pkColumnType}[] ids); + + /** + * 导出数据 + * @param all 待导出的数据 + * @param response / + * @throws IOException / + */ + void download(List<${className}Dto> all, HttpServletResponse response) throws IOException; +} \ No newline at end of file diff --git a/eladmin-generator/src/main/resources/template/admin/ServiceImpl.ftl b/eladmin-generator/src/main/resources/template/admin/ServiceImpl.ftl new file mode 100644 index 0000000..1ead242 --- /dev/null +++ b/eladmin-generator/src/main/resources/template/admin/ServiceImpl.ftl @@ -0,0 +1,157 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package ${package}.service.impl; + +import ${package}.domain.${className}; +<#if columns??> + <#list columns as column> + <#if column.columnKey = 'UNI'> + <#if column_index = 1> +import me.zhengjie.exception.EntityExistException; + + + + +import me.zhengjie.utils.ValidationUtil; +import me.zhengjie.utils.FileUtil; +import lombok.RequiredArgsConstructor; +import ${package}.repository.${className}Repository; +import ${package}.service.${className}Service; +import ${package}.service.dto.${className}Dto; +import ${package}.service.dto.${className}QueryCriteria; +import ${package}.service.mapstruct.${className}Mapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +<#if !auto && pkColumnType = 'Long'> +import cn.hutool.core.lang.Snowflake; +import cn.hutool.core.util.IdUtil; + +<#if !auto && pkColumnType = 'String'> +import cn.hutool.core.util.IdUtil; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import me.zhengjie.utils.PageUtil; +import me.zhengjie.utils.QueryHelp; +import java.util.List; +import java.util.Map; +import java.io.IOException; +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import me.zhengjie.utils.PageResult; + +/** +* @website https://eladmin.vip +* @description 服务实现 +* @author ${author} +**/ +@Service +@RequiredArgsConstructor +public class ${className}ServiceImpl implements ${className}Service { + + private final ${className}Repository ${changeClassName}Repository; + private final ${className}Mapper ${changeClassName}Mapper; + + @Override + public PageResult<${className}Dto> queryAll(${className}QueryCriteria criteria, Pageable pageable){ + Page<${className}> page = ${changeClassName}Repository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder),pageable); + return PageUtil.toPage(page.map(${changeClassName}Mapper::toDto)); + } + + @Override + public List<${className}Dto> queryAll(${className}QueryCriteria criteria){ + return ${changeClassName}Mapper.toDto(${changeClassName}Repository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder))); + } + + @Override + @Transactional + public ${className}Dto findById(${pkColumnType} ${pkChangeColName}) { + ${className} ${changeClassName} = ${changeClassName}Repository.findById(${pkChangeColName}).orElseGet(${className}::new); + ValidationUtil.isNull(${changeClassName}.get${pkCapitalColName}(),"${className}","${pkChangeColName}",${pkChangeColName}); + return ${changeClassName}Mapper.toDto(${changeClassName}); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void create(${className} resources) { +<#if !auto && pkColumnType = 'Long'> + Snowflake snowflake = IdUtil.createSnowflake(1, 1); + resources.set${pkCapitalColName}(snowflake.nextId()); + +<#if !auto && pkColumnType = 'String'> + resources.set${pkCapitalColName}(IdUtil.simpleUUID()); + +<#if columns??> + <#list columns as column> + <#if column.columnKey = 'UNI'> + if(${changeClassName}Repository.findBy${column.capitalColumnName}(resources.get${column.capitalColumnName}()) != null){ + throw new EntityExistException(${className}.class,"${column.columnName}",resources.get${column.capitalColumnName}()); + } + + + + ${changeClassName}Repository.save(resources); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(${className} resources) { + ${className} ${changeClassName} = ${changeClassName}Repository.findById(resources.get${pkCapitalColName}()).orElseGet(${className}::new); + ValidationUtil.isNull( ${changeClassName}.get${pkCapitalColName}(),"${className}","id",resources.get${pkCapitalColName}()); +<#if columns??> + <#list columns as column> + <#if column.columnKey = 'UNI'> + <#if column_index = 1> + ${className} ${changeClassName}1 = null; + + ${changeClassName}1 = ${changeClassName}Repository.findBy${column.capitalColumnName}(resources.get${column.capitalColumnName}()); + if(${changeClassName}1 != null && !${changeClassName}1.get${pkCapitalColName}().equals(${changeClassName}.get${pkCapitalColName}())){ + throw new EntityExistException(${className}.class,"${column.columnName}",resources.get${column.capitalColumnName}()); + } + + + + ${changeClassName}.copy(resources); + ${changeClassName}Repository.save(${changeClassName}); + } + + @Override + public void deleteAll(${pkColumnType}[] ids) { + for (${pkColumnType} ${pkChangeColName} : ids) { + ${changeClassName}Repository.deleteById(${pkChangeColName}); + } + } + + @Override + public void download(List<${className}Dto> all, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (${className}Dto ${changeClassName} : all) { + Map map = new LinkedHashMap<>(); + <#list columns as column> + <#if column.columnKey != 'PRI'> + <#if column.remark != ''> + map.put("${column.remark}", ${changeClassName}.get${column.capitalColumnName}()); + <#else> + map.put(" ${column.changeColumnName}", ${changeClassName}.get${column.capitalColumnName}()); + + + + list.add(map); + } + FileUtil.downloadExcel(list, response); + } +} \ No newline at end of file diff --git a/eladmin-generator/src/main/resources/template/front/api.ftl b/eladmin-generator/src/main/resources/template/front/api.ftl new file mode 100644 index 0000000..9587d0d --- /dev/null +++ b/eladmin-generator/src/main/resources/template/front/api.ftl @@ -0,0 +1,27 @@ +import request from '@/utils/request' + +export function add(data) { + return request({ + url: 'api/${changeClassName}', + method: 'post', + data + }) +} + +export function del(ids) { + return request({ + url: 'api/${changeClassName}/', + method: 'delete', + data: ids + }) +} + +export function edit(data) { + return request({ + url: 'api/${changeClassName}', + method: 'put', + data + }) +} + +export default { add, edit, del } diff --git a/eladmin-generator/src/main/resources/template/front/index.ftl b/eladmin-generator/src/main/resources/template/front/index.ftl new file mode 100644 index 0000000..9b7084c --- /dev/null +++ b/eladmin-generator/src/main/resources/template/front/index.ftl @@ -0,0 +1,169 @@ +<#--noinspection ALL--> + + + + + diff --git a/eladmin-logging/pom.xml b/eladmin-logging/pom.xml new file mode 100644 index 0000000..8613702 --- /dev/null +++ b/eladmin-logging/pom.xml @@ -0,0 +1,22 @@ + + + + eladmin + me.zhengjie + 2.7 + + 4.0.0 + + eladmin-logging + 日志模块 + + + + me.zhengjie + eladmin-common + 2.7 + + + \ No newline at end of file diff --git a/eladmin-logging/src/main/java/me/zhengjie/annotation/Log.java b/eladmin-logging/src/main/java/me/zhengjie/annotation/Log.java new file mode 100644 index 0000000..6c001cd --- /dev/null +++ b/eladmin-logging/src/main/java/me/zhengjie/annotation/Log.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Zheng Jie + * @date 2018-11-24 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Log { + String value() default ""; +} diff --git a/eladmin-logging/src/main/java/me/zhengjie/aspect/LogAspect.java b/eladmin-logging/src/main/java/me/zhengjie/aspect/LogAspect.java new file mode 100644 index 0000000..107aefc --- /dev/null +++ b/eladmin-logging/src/main/java/me/zhengjie/aspect/LogAspect.java @@ -0,0 +1,98 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.aspect; + +import lombok.extern.slf4j.Slf4j; +import me.zhengjie.domain.SysLog; +import me.zhengjie.service.SysLogService; +import me.zhengjie.utils.RequestHolder; +import me.zhengjie.utils.SecurityUtils; +import me.zhengjie.utils.StringUtils; +import me.zhengjie.utils.ThrowableUtil; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.stereotype.Component; +import javax.servlet.http.HttpServletRequest; + +/** + * @author Zheng Jie + * @date 2018-11-24 + */ +@Component +@Aspect +@Slf4j +public class LogAspect { + + private final SysLogService sysLogService; + + ThreadLocal currentTime = new ThreadLocal<>(); + + public LogAspect(SysLogService sysLogService) { + this.sysLogService = sysLogService; + } + + /** + * 配置切入点 + */ + @Pointcut("@annotation(me.zhengjie.annotation.Log)") + public void logPointcut() { + // 该方法无方法体,主要为了让同类中其他方法使用此切入点 + } + + /** + * 配置环绕通知,使用在方法logPointcut()上注册的切入点 + * + * @param joinPoint join point for advice + */ + @Around("logPointcut()") + public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { + Object result; + currentTime.set(System.currentTimeMillis()); + result = joinPoint.proceed(); + SysLog sysLog = new SysLog("INFO",System.currentTimeMillis() - currentTime.get()); + currentTime.remove(); + HttpServletRequest request = RequestHolder.getHttpServletRequest(); + sysLogService.save(getUsername(), StringUtils.getBrowser(request), StringUtils.getIp(request),joinPoint, sysLog); + return result; + } + + /** + * 配置异常通知 + * + * @param joinPoint join point for advice + * @param e exception + */ + @AfterThrowing(pointcut = "logPointcut()", throwing = "e") + public void logAfterThrowing(JoinPoint joinPoint, Throwable e) { + SysLog sysLog = new SysLog("ERROR",System.currentTimeMillis() - currentTime.get()); + currentTime.remove(); + sysLog.setExceptionDetail(ThrowableUtil.getStackTrace(e).getBytes()); + HttpServletRequest request = RequestHolder.getHttpServletRequest(); + sysLogService.save(getUsername(), StringUtils.getBrowser(request), StringUtils.getIp(request), (ProceedingJoinPoint)joinPoint, sysLog); + } + + public String getUsername() { + try { + return SecurityUtils.getCurrentUsername(); + }catch (Exception e){ + return ""; + } + } +} diff --git a/eladmin-logging/src/main/java/me/zhengjie/domain/SysLog.java b/eladmin-logging/src/main/java/me/zhengjie/domain/SysLog.java new file mode 100644 index 0000000..a6b2e47 --- /dev/null +++ b/eladmin-logging/src/main/java/me/zhengjie/domain/SysLog.java @@ -0,0 +1,80 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.domain; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import javax.persistence.*; +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @author Zheng Jie + * @date 2018-11-24 + */ +@Entity +@Getter +@Setter +@Table(name = "sys_log") +@NoArgsConstructor +public class SysLog implements Serializable { + + @Id + @Column(name = "log_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** 操作用户 */ + private String username; + + /** 描述 */ + private String description; + + /** 方法名 */ + private String method; + + /** 参数 */ + private String params; + + /** 日志类型 */ + private String logType; + + /** 请求ip */ + private String requestIp; + + /** 地址 */ + private String address; + + /** 浏览器 */ + private String browser; + + /** 请求耗时 */ + private Long time; + + /** 异常详细 */ + private byte[] exceptionDetail; + + /** 创建日期 */ + @CreationTimestamp + private Timestamp createTime; + + public SysLog(String logType, Long time) { + this.logType = logType; + this.time = time; + } +} diff --git a/eladmin-logging/src/main/java/me/zhengjie/repository/LogRepository.java b/eladmin-logging/src/main/java/me/zhengjie/repository/LogRepository.java new file mode 100644 index 0000000..805f97b --- /dev/null +++ b/eladmin-logging/src/main/java/me/zhengjie/repository/LogRepository.java @@ -0,0 +1,39 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.repository; + +import me.zhengjie.domain.SysLog; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +/** + * @author Zheng Jie + * @date 2018-11-24 + */ +@Repository +public interface LogRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * 根据日志类型删除信息 + * @param logType 日志类型 + */ + @Modifying + @Query(value = "delete from sys_log where log_type = ?1", nativeQuery = true) + void deleteByLogType(String logType); +} diff --git a/eladmin-logging/src/main/java/me/zhengjie/rest/SysLogController.java b/eladmin-logging/src/main/java/me/zhengjie/rest/SysLogController.java new file mode 100644 index 0000000..fa17370 --- /dev/null +++ b/eladmin-logging/src/main/java/me/zhengjie/rest/SysLogController.java @@ -0,0 +1,111 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import me.zhengjie.annotation.Log; +import me.zhengjie.service.SysLogService; +import me.zhengjie.service.dto.SysLogQueryCriteria; +import me.zhengjie.service.dto.SysLogSmallDto; +import me.zhengjie.utils.PageResult; +import me.zhengjie.utils.SecurityUtils; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author Zheng Jie + * @date 2018-11-24 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/logs") +@Api(tags = "系统:日志管理") +public class SysLogController { + + private final SysLogService sysLogService; + + @Log("导出数据") + @ApiOperation("导出数据") + @GetMapping(value = "/download") + @PreAuthorize("@el.check()") + public void exportLog(HttpServletResponse response, SysLogQueryCriteria criteria) throws IOException { + criteria.setLogType("INFO"); + sysLogService.download(sysLogService.queryAll(criteria), response); + } + + @Log("导出错误数据") + @ApiOperation("导出错误数据") + @GetMapping(value = "/error/download") + @PreAuthorize("@el.check()") + public void exportErrorLog(HttpServletResponse response, SysLogQueryCriteria criteria) throws IOException { + criteria.setLogType("ERROR"); + sysLogService.download(sysLogService.queryAll(criteria), response); + } + @GetMapping + @ApiOperation("日志查询") + @PreAuthorize("@el.check()") + public ResponseEntity queryLog(SysLogQueryCriteria criteria, Pageable pageable){ + criteria.setLogType("INFO"); + return new ResponseEntity<>(sysLogService.queryAll(criteria,pageable), HttpStatus.OK); + } + + @GetMapping(value = "/user") + @ApiOperation("用户日志查询") + public ResponseEntity> queryUserLog(SysLogQueryCriteria criteria, Pageable pageable){ + criteria.setLogType("INFO"); + criteria.setUsername(SecurityUtils.getCurrentUsername()); + return new ResponseEntity<>(sysLogService.queryAllByUser(criteria,pageable), HttpStatus.OK); + } + + @GetMapping(value = "/error") + @ApiOperation("错误日志查询") + @PreAuthorize("@el.check()") + public ResponseEntity queryErrorLog(SysLogQueryCriteria criteria, Pageable pageable){ + criteria.setLogType("ERROR"); + return new ResponseEntity<>(sysLogService.queryAll(criteria,pageable), HttpStatus.OK); + } + + @GetMapping(value = "/error/{id}") + @ApiOperation("日志异常详情查询") + @PreAuthorize("@el.check()") + public ResponseEntity queryErrorLogDetail(@PathVariable Long id){ + return new ResponseEntity<>(sysLogService.findByErrDetail(id), HttpStatus.OK); + } + @DeleteMapping(value = "/del/error") + @Log("删除所有ERROR日志") + @ApiOperation("删除所有ERROR日志") + @PreAuthorize("@el.check()") + public ResponseEntity delAllErrorLog(){ + sysLogService.delAllByError(); + return new ResponseEntity<>(HttpStatus.OK); + } + + @DeleteMapping(value = "/del/info") + @Log("删除所有INFO日志") + @ApiOperation("删除所有INFO日志") + @PreAuthorize("@el.check()") + public ResponseEntity delAllInfoLog(){ + sysLogService.delAllByInfo(); + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/eladmin-logging/src/main/java/me/zhengjie/service/SysLogService.java b/eladmin-logging/src/main/java/me/zhengjie/service/SysLogService.java new file mode 100644 index 0000000..80f2d1c --- /dev/null +++ b/eladmin-logging/src/main/java/me/zhengjie/service/SysLogService.java @@ -0,0 +1,94 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service; + +import me.zhengjie.domain.SysLog; +import me.zhengjie.service.dto.SysLogQueryCriteria; +import me.zhengjie.service.dto.SysLogSmallDto; +import me.zhengjie.utils.PageResult; +import org.aspectj.lang.ProceedingJoinPoint; +import org.springframework.data.domain.Pageable; +import org.springframework.scheduling.annotation.Async; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; + +/** + * @author Zheng Jie + * @date 2018-11-24 + */ +public interface SysLogService { + + /** + * 分页查询 + * @param criteria 查询条件 + * @param pageable 分页参数 + * @return / + */ + Object queryAll(SysLogQueryCriteria criteria, Pageable pageable); + + /** + * 查询全部数据 + * @param criteria 查询条件 + * @return / + */ + List queryAll(SysLogQueryCriteria criteria); + + /** + * 查询用户日志 + * @param criteria 查询条件 + * @param pageable 分页参数 + * @return - + */ + PageResult queryAllByUser(SysLogQueryCriteria criteria, Pageable pageable); + + /** + * 保存日志数据 + * @param username 用户 + * @param browser 浏览器 + * @param ip 请求IP + * @param joinPoint / + * @param sysLog 日志实体 + */ + @Async + void save(String username, String browser, String ip, ProceedingJoinPoint joinPoint, SysLog sysLog); + + /** + * 查询异常详情 + * @param id 日志ID + * @return Object + */ + Object findByErrDetail(Long id); + + /** + * 导出日志 + * @param sysLogs 待导出的数据 + * @param response / + * @throws IOException / + */ + void download(List sysLogs, HttpServletResponse response) throws IOException; + + /** + * 删除所有错误日志 + */ + void delAllByError(); + + /** + * 删除所有INFO日志 + */ + void delAllByInfo(); +} diff --git a/eladmin-logging/src/main/java/me/zhengjie/service/dto/SysLogErrorDto.java b/eladmin-logging/src/main/java/me/zhengjie/service/dto/SysLogErrorDto.java new file mode 100644 index 0000000..1fde4fb --- /dev/null +++ b/eladmin-logging/src/main/java/me/zhengjie/service/dto/SysLogErrorDto.java @@ -0,0 +1,46 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service.dto; + +import lombok.Data; +import java.io.Serializable; +import java.sql.Timestamp; + +/** +* @author Zheng Jie +* @date 2019-5-22 +*/ +@Data +public class SysLogErrorDto implements Serializable { + + private Long id; + + private String username; + + private String description; + + private String method; + + private String params; + + private String browser; + + private String requestIp; + + private String address; + + private Timestamp createTime; +} \ No newline at end of file diff --git a/eladmin-logging/src/main/java/me/zhengjie/service/dto/SysLogQueryCriteria.java b/eladmin-logging/src/main/java/me/zhengjie/service/dto/SysLogQueryCriteria.java new file mode 100644 index 0000000..0588f64 --- /dev/null +++ b/eladmin-logging/src/main/java/me/zhengjie/service/dto/SysLogQueryCriteria.java @@ -0,0 +1,42 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service.dto; + +import lombok.Data; +import me.zhengjie.annotation.Query; +import java.sql.Timestamp; +import java.util.List; + +/** + * 日志查询类 + * @author Zheng Jie + * @date 2019-6-4 09:23:07 + */ +@Data +public class SysLogQueryCriteria { + + @Query(blurry = "username,description,address,requestIp,method,params") + private String blurry; + + @Query + private String username; + + @Query + private String logType; + + @Query(type = Query.Type.BETWEEN) + private List createTime; +} diff --git a/eladmin-logging/src/main/java/me/zhengjie/service/dto/SysLogSmallDto.java b/eladmin-logging/src/main/java/me/zhengjie/service/dto/SysLogSmallDto.java new file mode 100644 index 0000000..d7312cd --- /dev/null +++ b/eladmin-logging/src/main/java/me/zhengjie/service/dto/SysLogSmallDto.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service.dto; + +import lombok.Data; +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @author Zheng Jie + * @date 2019-5-22 + */ +@Data +public class SysLogSmallDto implements Serializable { + + private String description; + + private String requestIp; + + private Long time; + + private String address; + + private String browser; + + private Timestamp createTime; +} diff --git a/eladmin-logging/src/main/java/me/zhengjie/service/impl/SysLogServiceImpl.java b/eladmin-logging/src/main/java/me/zhengjie/service/impl/SysLogServiceImpl.java new file mode 100644 index 0000000..95ea4a0 --- /dev/null +++ b/eladmin-logging/src/main/java/me/zhengjie/service/impl/SysLogServiceImpl.java @@ -0,0 +1,180 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service.impl; + +import cn.hutool.core.lang.Dict; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import lombok.RequiredArgsConstructor; +import me.zhengjie.domain.SysLog; +import me.zhengjie.repository.LogRepository; +import me.zhengjie.service.SysLogService; +import me.zhengjie.service.dto.SysLogQueryCriteria; +import me.zhengjie.service.dto.SysLogSmallDto; +import me.zhengjie.service.mapstruct.LogErrorMapper; +import me.zhengjie.service.mapstruct.LogSmallMapper; +import me.zhengjie.utils.*; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.*; + +/** + * @author Zheng Jie + * @date 2018-11-24 + */ +@Service +@RequiredArgsConstructor +public class SysLogServiceImpl implements SysLogService { + private final LogRepository logRepository; + private final LogErrorMapper logErrorMapper; + private final LogSmallMapper logSmallMapper; + + @Override + public Object queryAll(SysLogQueryCriteria criteria, Pageable pageable) { + Page page = logRepository.findAll(((root, criteriaQuery, cb) -> QueryHelp.getPredicate(root, criteria, cb)), pageable); + String status = "ERROR"; + if (status.equals(criteria.getLogType())) { + return PageUtil.toPage(page.map(logErrorMapper::toDto)); + } + return PageUtil.toPage(page); + } + + @Override + public List queryAll(SysLogQueryCriteria criteria) { + return logRepository.findAll(((root, criteriaQuery, cb) -> QueryHelp.getPredicate(root, criteria, cb))); + } + + @Override + public PageResult queryAllByUser(SysLogQueryCriteria criteria, Pageable pageable) { + Page page = logRepository.findAll(((root, criteriaQuery, cb) -> QueryHelp.getPredicate(root, criteria, cb)), pageable); + return PageUtil.toPage(page.map(logSmallMapper::toDto)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void save(String username, String browser, String ip, ProceedingJoinPoint joinPoint, SysLog sysLog) { + if (sysLog == null) { + throw new IllegalArgumentException("Log 不能为 null!"); + } + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + me.zhengjie.annotation.Log aopLog = method.getAnnotation(me.zhengjie.annotation.Log.class); + + // 方法路径 + String methodName = joinPoint.getTarget().getClass().getName() + "." + signature.getName() + "()"; + + // 描述 + sysLog.setDescription(aopLog.value()); + + sysLog.setRequestIp(ip); + sysLog.setAddress(StringUtils.getCityInfo(sysLog.getRequestIp())); + sysLog.setMethod(methodName); + sysLog.setUsername(username); + sysLog.setParams(getParameter(method, joinPoint.getArgs())); + // 记录登录用户,隐藏密码信息 + if(signature.getName().equals("login") && StringUtils.isNotEmpty(sysLog.getParams())){ + JSONObject obj = JSON.parseObject(sysLog.getParams()); + sysLog.setUsername(obj.getString("username")); + sysLog.setParams(JSON.toJSONString(Dict.create().set("username", sysLog.getUsername()))); + } + sysLog.setBrowser(browser); + logRepository.save(sysLog); + } + + /** + * 根据方法和传入的参数获取请求参数 + */ + private String getParameter(Method method, Object[] args) { + List argList = new ArrayList<>(); + Parameter[] parameters = method.getParameters(); + for (int i = 0; i < parameters.length; i++) { + // 过滤掉不能序列化的类型: MultiPartFile + if (args[i] instanceof MultipartFile) { + continue; + } + //将RequestBody注解修饰的参数作为请求参数 + RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class); + if (requestBody != null) { + argList.add(args[i]); + } + //将RequestParam注解修饰的参数作为请求参数 + RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class); + if (requestParam != null) { + Map map = new HashMap<>(2); + String key = parameters[i].getName(); + if (!StringUtils.isEmpty(requestParam.value())) { + key = requestParam.value(); + } + map.put(key, args[i]); + argList.add(map); + } + } + if (argList.isEmpty()) { + return ""; + } + return argList.size() == 1 ? JSON.toJSONString(argList.get(0)) : JSON.toJSONString(argList); + } + + @Override + public Object findByErrDetail(Long id) { + SysLog sysLog = logRepository.findById(id).orElseGet(SysLog::new); + ValidationUtil.isNull(sysLog.getId(), "Log", "id", id); + byte[] details = sysLog.getExceptionDetail(); + return Dict.create().set("exception", new String(ObjectUtil.isNotNull(details) ? details : "".getBytes())); + } + + @Override + public void download(List sysLogs, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (SysLog sysLog : sysLogs) { + Map map = new LinkedHashMap<>(); + map.put("用户名", sysLog.getUsername()); + map.put("IP", sysLog.getRequestIp()); + map.put("IP来源", sysLog.getAddress()); + map.put("描述", sysLog.getDescription()); + map.put("浏览器", sysLog.getBrowser()); + map.put("请求耗时/毫秒", sysLog.getTime()); + map.put("异常详情", new String(ObjectUtil.isNotNull(sysLog.getExceptionDetail()) ? sysLog.getExceptionDetail() : "".getBytes())); + map.put("创建日期", sysLog.getCreateTime()); + list.add(map); + } + FileUtil.downloadExcel(list, response); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delAllByError() { + logRepository.deleteByLogType("ERROR"); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delAllByInfo() { + logRepository.deleteByLogType("INFO"); + } +} diff --git a/eladmin-logging/src/main/java/me/zhengjie/service/mapstruct/LogErrorMapper.java b/eladmin-logging/src/main/java/me/zhengjie/service/mapstruct/LogErrorMapper.java new file mode 100644 index 0000000..c777d11 --- /dev/null +++ b/eladmin-logging/src/main/java/me/zhengjie/service/mapstruct/LogErrorMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import me.zhengjie.domain.SysLog; +import me.zhengjie.service.dto.SysLogErrorDto; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** + * @author Zheng Jie + * @date 2019-5-22 + */ +@Mapper(componentModel = "spring",unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface LogErrorMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/eladmin-logging/src/main/java/me/zhengjie/service/mapstruct/LogSmallMapper.java b/eladmin-logging/src/main/java/me/zhengjie/service/mapstruct/LogSmallMapper.java new file mode 100644 index 0000000..eeb5433 --- /dev/null +++ b/eladmin-logging/src/main/java/me/zhengjie/service/mapstruct/LogSmallMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import me.zhengjie.domain.SysLog; +import me.zhengjie.service.dto.SysLogSmallDto; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** + * @author Zheng Jie + * @date 2019-5-22 + */ +@Mapper(componentModel = "spring",unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface LogSmallMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/eladmin-system/pom.xml b/eladmin-system/pom.xml new file mode 100644 index 0000000..9119bc1 --- /dev/null +++ b/eladmin-system/pom.xml @@ -0,0 +1,108 @@ + + + + eladmin + me.zhengjie + 2.7 + + 4.0.0 + + eladmin-system + 核心模块 + + + 0.11.5 + + 5.8.0 + + + + + + me.zhengjie + eladmin-generator + 2.7 + + + me.zhengjie + eladmin-common + + + + + + + me.zhengjie + eladmin-tools + 2.7 + + + + + org.springframework.boot + spring-boot-starter-websocket + + + + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + + + + + org.quartz-scheduler + quartz + + + + + ch.ethz.ganymed + ganymed-ssh2 + build210 + + + com.jcraft + jsch + 0.1.55 + + + + + com.github.oshi + oshi-core + 6.1.4 + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + diff --git a/eladmin-system/src/main/java/me/zhengjie/AppRun.java b/eladmin-system/src/main/java/me/zhengjie/AppRun.java new file mode 100644 index 0000000..ad12601 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/AppRun.java @@ -0,0 +1,66 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie; + +import io.swagger.annotations.Api; +import me.zhengjie.annotation.rest.AnonymousGetMapping; +import me.zhengjie.utils.SpringContextHolder; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.ApplicationPidFileWriter; +import org.springframework.context.annotation.Bean; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.web.bind.annotation.RestController; + +/** + * 开启审计功能 -> @EnableJpaAuditing + * + * @author Zheng Jie + * @date 2018/11/15 9:20:19 + */ +@EnableAsync +@RestController +@Api(hidden = true) +@SpringBootApplication +@EnableTransactionManagement +@EnableJpaAuditing(auditorAwareRef = "auditorAware") +public class AppRun { + + public static void main(String[] args) { + SpringApplication springApplication = new SpringApplication(AppRun.class); + // 监控应用的PID,启动时可指定PID路径:--spring.pid.file=/home/eladmin/app.pid + // 或者在 application.yml 添加文件路径,方便 kill,kill `cat /home/eladmin/app.pid` + springApplication.addListeners(new ApplicationPidFileWriter()); + springApplication.run(args); + } + + @Bean + public SpringContextHolder springContextHolder() { + return new SpringContextHolder(); + } + + /** + * 访问首页提示 + * + * @return / + */ + @AnonymousGetMapping("/") + public String index() { + return "Backend service started successfully"; + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/config/ConfigurerAdapter.java b/eladmin-system/src/main/java/me/zhengjie/config/ConfigurerAdapter.java new file mode 100644 index 0000000..a0093d6 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/config/ConfigurerAdapter.java @@ -0,0 +1,88 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.config; + +import com.alibaba.fastjson.serializer.SerializerFeature; +import com.alibaba.fastjson.support.config.FastJsonConfig; +import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +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.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * WebMvcConfigurer + * + * @author Zheng Jie + * @date 2018-11-30 + */ +@Configuration +@EnableWebMvc +public class ConfigurerAdapter implements WebMvcConfigurer { + + /** 文件配置 */ + private final FileProperties properties; + + public ConfigurerAdapter(FileProperties properties) { + this.properties = properties; + } + + @Bean + public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOriginPattern("*"); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + FileProperties.ElPath path = properties.getPath(); + String avatarUtl = "file:" + path.getAvatar().replace("\\","/"); + String pathUtl = "file:" + path.getPath().replace("\\","/"); + registry.addResourceHandler("/avatar/**").addResourceLocations(avatarUtl).setCachePeriod(0); + registry.addResourceHandler("/file/**").addResourceLocations(pathUtl).setCachePeriod(0); + registry.addResourceHandler("/**").addResourceLocations("classpath:/META-INF/resources/").setCachePeriod(0); + } + + @Override + public void configureMessageConverters(List> converters) { + // 使用 fastjson 序列化,会导致 @JsonIgnore 失效,可以使用 @JSONField(serialize = false) 替换 + FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter(); + List supportMediaTypeList = new ArrayList<>(); + supportMediaTypeList.add(MediaType.APPLICATION_JSON); + FastJsonConfig config = new FastJsonConfig(); + config.setDateFormat("yyyy-MM-dd HH:mm:ss"); + config.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect); + converter.setFastJsonConfig(config); + converter.setSupportedMediaTypes(supportMediaTypeList); + converter.setDefaultCharset(StandardCharsets.UTF_8); + converters.add(converter); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/config/RelaxedQueryCharsConnectorCustomizer.java b/eladmin-system/src/main/java/me/zhengjie/config/RelaxedQueryCharsConnectorCustomizer.java new file mode 100644 index 0000000..14a6b20 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/config/RelaxedQueryCharsConnectorCustomizer.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2023 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.config; + +import org.apache.catalina.connector.Connector; +import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer; +import org.springframework.context.annotation.Configuration; + +/** + * @author bearBoy80 + */ +@Configuration(proxyBeanMethods = false) +public class RelaxedQueryCharsConnectorCustomizer implements TomcatConnectorCustomizer { + @Override + public void customize(Connector connector) { + connector.setProperty("relaxedQueryChars", "[]{}"); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/config/WebSocketConfig.java b/eladmin-system/src/main/java/me/zhengjie/config/WebSocketConfig.java new file mode 100644 index 0000000..f55f5c6 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/config/WebSocketConfig.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.server.standard.ServerEndpointExporter; + +/** + * @author ZhangHouYing + * @date 2019-08-24 15:44 + */ +@Configuration +public class WebSocketConfig { + + @Bean + public ServerEndpointExporter serverEndpointExporter() { + return new ServerEndpointExporter(); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/config/thread/AsyncTaskProperties.java b/eladmin-system/src/main/java/me/zhengjie/config/thread/AsyncTaskProperties.java new file mode 100644 index 0000000..6e075ea --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/config/thread/AsyncTaskProperties.java @@ -0,0 +1,59 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.config.thread; + +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 线程池配置属性类 + * @author https://juejin.im/entry/5abb8f6951882555677e9da2 + * @date 2019年10月31日14:58:18 + */ +@Data +@Component +public class AsyncTaskProperties { + + public static int corePoolSize; + + public static int maxPoolSize; + + public static int keepAliveSeconds; + + public static int queueCapacity; + + @Value("${task.pool.core-pool-size}") + public void setCorePoolSize(int corePoolSize) { + AsyncTaskProperties.corePoolSize = corePoolSize; + } + + @Value("${task.pool.max-pool-size}") + public void setMaxPoolSize(int maxPoolSize) { + AsyncTaskProperties.maxPoolSize = maxPoolSize; + } + + @Value("${task.pool.keep-alive-seconds}") + public void setKeepAliveSeconds(int keepAliveSeconds) { + AsyncTaskProperties.keepAliveSeconds = keepAliveSeconds; + } + + @Value("${task.pool.queue-capacity}") + public void setQueueCapacity(int queueCapacity) { + AsyncTaskProperties.queueCapacity = queueCapacity; + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/config/thread/CustomExecutorConfig.java b/eladmin-system/src/main/java/me/zhengjie/config/thread/CustomExecutorConfig.java new file mode 100644 index 0000000..ca8a8bc --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/config/thread/CustomExecutorConfig.java @@ -0,0 +1,52 @@ +package me.zhengjie.config.thread; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 创建自定义的线程池 + * @author Zheng Jie + * @description + * @date 2023-06-08 + **/ +@Configuration +public class CustomExecutorConfig { + + /** + * 自定义线程池,用法 @Async + * @return Executor + */ + @Bean + @Primary + public Executor elAsync() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(AsyncTaskProperties.corePoolSize); + executor.setMaxPoolSize(AsyncTaskProperties.maxPoolSize); + executor.setQueueCapacity(AsyncTaskProperties.queueCapacity); + executor.setThreadNamePrefix("el-async-"); + executor.setKeepAliveSeconds(AsyncTaskProperties.keepAliveSeconds); + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + executor.initialize(); + return executor; + } + + /** + * 自定义线程池,用法 @Async("otherAsync") + * @return Executor + */ + @Bean + public Executor otherAsync() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(15); + executor.setQueueCapacity(50); + executor.setKeepAliveSeconds(AsyncTaskProperties.keepAliveSeconds); + executor.setThreadNamePrefix("el-task-"); + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + executor.initialize(); + return executor; + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/domain/App.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/domain/App.java new file mode 100644 index 0000000..cf5789c --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/domain/App.java @@ -0,0 +1,67 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.domain; + +import io.swagger.annotations.ApiModelProperty; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import lombok.Getter; +import lombok.Setter; +import me.zhengjie.base.BaseEntity; +import javax.persistence.*; +import java.io.Serializable; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Entity +@Getter +@Setter +@Table(name="mnt_app") +public class App extends BaseEntity implements Serializable { + + @Id + @Column(name = "app_id") + @ApiModelProperty(value = "ID", hidden = true) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ApiModelProperty(value = "名称") + private String name; + + @ApiModelProperty(value = "端口") + private int port; + + @ApiModelProperty(value = "上传路径") + private String uploadPath; + + @ApiModelProperty(value = "部署路径") + private String deployPath; + + @ApiModelProperty(value = "备份路径") + private String backupPath; + + @ApiModelProperty(value = "启动脚本") + private String startScript; + + @ApiModelProperty(value = "部署脚本") + private String deployScript; + + public void copy(App source){ + BeanUtil.copyProperties(source,this, CopyOptions.create().setIgnoreNullValue(true)); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/domain/Database.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/domain/Database.java new file mode 100644 index 0000000..6b3a68a --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/domain/Database.java @@ -0,0 +1,57 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.domain; + +import io.swagger.annotations.ApiModelProperty; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import lombok.Getter; +import lombok.Setter; +import me.zhengjie.base.BaseEntity; +import javax.persistence.*; +import java.io.Serializable; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Entity +@Getter +@Setter +@Table(name="mnt_database") +public class Database extends BaseEntity implements Serializable { + + @Id + @Column(name = "db_id") + @ApiModelProperty(value = "ID", hidden = true) + private String id; + + @ApiModelProperty(value = "数据库名称") + private String name; + + @ApiModelProperty(value = "数据库连接地址") + private String jdbcUrl; + + @ApiModelProperty(value = "数据库密码") + private String pwd; + + @ApiModelProperty(value = "用户名") + private String userName; + + public void copy(Database source){ + BeanUtil.copyProperties(source,this, CopyOptions.create().setIgnoreNullValue(true)); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/domain/Deploy.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/domain/Deploy.java new file mode 100644 index 0000000..bcf61e5 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/domain/Deploy.java @@ -0,0 +1,59 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.domain; + +import io.swagger.annotations.ApiModelProperty; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import lombok.Getter; +import lombok.Setter; +import me.zhengjie.base.BaseEntity; +import javax.persistence.*; +import java.io.Serializable; +import java.util.Set; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Entity +@Getter +@Setter +@Table(name="mnt_deploy") +public class Deploy extends BaseEntity implements Serializable { + + @Id + @Column(name = "deploy_id") + @ApiModelProperty(value = "ID", hidden = true) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToMany + @ApiModelProperty(name = "服务器", hidden = true) + @JoinTable(name = "mnt_deploy_server", + joinColumns = {@JoinColumn(name = "deploy_id",referencedColumnName = "deploy_id")}, + inverseJoinColumns = {@JoinColumn(name = "server_id",referencedColumnName = "server_id")}) + private Set deploys; + + @ManyToOne + @JoinColumn(name = "app_id") + @ApiModelProperty(value = "应用编号") + private App app; + + public void copy(Deploy source){ + BeanUtil.copyProperties(source,this, CopyOptions.create().setIgnoreNullValue(true)); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/domain/DeployHistory.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/domain/DeployHistory.java new file mode 100644 index 0000000..6e07e13 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/domain/DeployHistory.java @@ -0,0 +1,62 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.domain; + +import io.swagger.annotations.ApiModelProperty; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import javax.persistence.*; +import java.io.Serializable; +import java.sql.Timestamp; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Entity +@Getter +@Setter +@Table(name="mnt_deploy_history") +public class DeployHistory implements Serializable { + + @Id + @Column(name = "history_id") + @ApiModelProperty(value = "ID", hidden = true) + private String id; + + @ApiModelProperty(value = "应用名称") + private String appName; + + @ApiModelProperty(value = "IP") + private String ip; + + @CreationTimestamp + @ApiModelProperty(value = "部署时间") + private Timestamp deployDate; + + @ApiModelProperty(value = "部署者") + private String deployUser; + + @ApiModelProperty(value = "部署ID") + private Long deployId; + + public void copy(DeployHistory source){ + BeanUtil.copyProperties(source,this, CopyOptions.create().setIgnoreNullValue(true)); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/domain/ServerDeploy.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/domain/ServerDeploy.java new file mode 100644 index 0000000..f523562 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/domain/ServerDeploy.java @@ -0,0 +1,80 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.domain; + +import io.swagger.annotations.ApiModelProperty; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import lombok.Getter; +import lombok.Setter; +import me.zhengjie.base.BaseEntity; +import javax.persistence.*; +import java.io.Serializable; +import java.util.Objects; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Entity +@Getter +@Setter +@Table(name="mnt_server") +public class ServerDeploy extends BaseEntity implements Serializable { + + @Id + @Column(name = "server_id") + @ApiModelProperty(value = "ID", hidden = true) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ApiModelProperty(value = "服务器名称") + private String name; + + @ApiModelProperty(value = "IP") + private String ip; + + @ApiModelProperty(value = "端口") + private Integer port; + + @ApiModelProperty(value = "账号") + private String account; + + @ApiModelProperty(value = "密码") + private String password; + + public void copy(ServerDeploy source){ + BeanUtil.copyProperties(source,this, CopyOptions.create().setIgnoreNullValue(true)); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ServerDeploy that = (ServerDeploy) o; + return Objects.equals(id, that.id) && + Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/repository/AppRepository.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/repository/AppRepository.java new file mode 100644 index 0000000..41e5f5c --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/repository/AppRepository.java @@ -0,0 +1,27 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.repository; + +import me.zhengjie.modules.mnt.domain.App; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +public interface AppRepository extends JpaRepository, JpaSpecificationExecutor { +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/repository/DatabaseRepository.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/repository/DatabaseRepository.java new file mode 100644 index 0000000..695e0ad --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/repository/DatabaseRepository.java @@ -0,0 +1,27 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.repository; + +import me.zhengjie.modules.mnt.domain.Database; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +public interface DatabaseRepository extends JpaRepository, JpaSpecificationExecutor { +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/repository/DeployHistoryRepository.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/repository/DeployHistoryRepository.java new file mode 100644 index 0000000..3c8980e --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/repository/DeployHistoryRepository.java @@ -0,0 +1,27 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.repository; + +import me.zhengjie.modules.mnt.domain.DeployHistory; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +public interface DeployHistoryRepository extends JpaRepository, JpaSpecificationExecutor { +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/repository/DeployRepository.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/repository/DeployRepository.java new file mode 100644 index 0000000..2ea4498 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/repository/DeployRepository.java @@ -0,0 +1,27 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.repository; + +import me.zhengjie.modules.mnt.domain.Deploy; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +public interface DeployRepository extends JpaRepository, JpaSpecificationExecutor { +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/repository/ServerDeployRepository.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/repository/ServerDeployRepository.java new file mode 100644 index 0000000..4ca336c --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/repository/ServerDeployRepository.java @@ -0,0 +1,34 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.repository; + +import me.zhengjie.modules.mnt.domain.ServerDeploy; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +public interface ServerDeployRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * 根据IP查询 + * @param ip / + * @return / + */ + ServerDeploy findByIp(String ip); +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/rest/AppController.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/rest/AppController.java new file mode 100644 index 0000000..5dac15e --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/rest/AppController.java @@ -0,0 +1,89 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import me.zhengjie.annotation.Log; +import me.zhengjie.modules.mnt.domain.App; +import me.zhengjie.modules.mnt.service.AppService; +import me.zhengjie.modules.mnt.service.dto.AppDto; +import me.zhengjie.modules.mnt.service.dto.AppQueryCriteria; +import me.zhengjie.utils.PageResult; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Set; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@RestController +@RequiredArgsConstructor +@Api(tags = "运维:应用管理") +@RequestMapping("/api/app") +public class AppController { + + private final AppService appService; + + @ApiOperation("导出应用数据") + @GetMapping(value = "/download") + @PreAuthorize("@el.check('app:list')") + public void exportApp(HttpServletResponse response, AppQueryCriteria criteria) throws IOException { + appService.download(appService.queryAll(criteria), response); + } + + @ApiOperation(value = "查询应用") + @GetMapping + @PreAuthorize("@el.check('app:list')") + public ResponseEntity> queryApp(AppQueryCriteria criteria, Pageable pageable){ + return new ResponseEntity<>(appService.queryAll(criteria,pageable),HttpStatus.OK); + } + + @Log("新增应用") + @ApiOperation(value = "新增应用") + @PostMapping + @PreAuthorize("@el.check('app:add')") + public ResponseEntity createApp(@Validated @RequestBody App resources){ + appService.create(resources); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @Log("修改应用") + @ApiOperation(value = "修改应用") + @PutMapping + @PreAuthorize("@el.check('app:edit')") + public ResponseEntity updateApp(@Validated @RequestBody App resources){ + appService.update(resources); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @Log("删除应用") + @ApiOperation(value = "删除应用") + @DeleteMapping + @PreAuthorize("@el.check('app:del')") + public ResponseEntity deleteApp(@RequestBody Set ids){ + appService.delete(ids); + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/rest/DatabaseController.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/rest/DatabaseController.java new file mode 100644 index 0000000..0ce1a4e --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/rest/DatabaseController.java @@ -0,0 +1,124 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import me.zhengjie.annotation.Log; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.modules.mnt.domain.Database; +import me.zhengjie.modules.mnt.service.DatabaseService; +import me.zhengjie.modules.mnt.service.dto.DatabaseDto; +import me.zhengjie.modules.mnt.service.dto.DatabaseQueryCriteria; +import me.zhengjie.modules.mnt.util.SqlUtils; +import me.zhengjie.utils.FileUtil; +import me.zhengjie.utils.PageResult; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.util.Set; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Api(tags = "运维:数据库管理") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/database") +public class DatabaseController { + + private final String fileSavePath = FileUtil.getTmpDirPath()+"/"; + private final DatabaseService databaseService; + + @ApiOperation("导出数据库数据") + @GetMapping(value = "/download") + @PreAuthorize("@el.check('database:list')") + public void exportDatabase(HttpServletResponse response, DatabaseQueryCriteria criteria) throws IOException { + databaseService.download(databaseService.queryAll(criteria), response); + } + + @ApiOperation(value = "查询数据库") + @GetMapping + @PreAuthorize("@el.check('database:list')") + public ResponseEntity> queryDatabase(DatabaseQueryCriteria criteria, Pageable pageable){ + return new ResponseEntity<>(databaseService.queryAll(criteria,pageable),HttpStatus.OK); + } + + @Log("新增数据库") + @ApiOperation(value = "新增数据库") + @PostMapping + @PreAuthorize("@el.check('database:add')") + public ResponseEntity createDatabase(@Validated @RequestBody Database resources){ + databaseService.create(resources); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @Log("修改数据库") + @ApiOperation(value = "修改数据库") + @PutMapping + @PreAuthorize("@el.check('database:edit')") + public ResponseEntity updateDatabase(@Validated @RequestBody Database resources){ + databaseService.update(resources); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @Log("删除数据库") + @ApiOperation(value = "删除数据库") + @DeleteMapping + @PreAuthorize("@el.check('database:del')") + public ResponseEntity deleteDatabase(@RequestBody Set ids){ + databaseService.delete(ids); + return new ResponseEntity<>(HttpStatus.OK); + } + + @Log("测试数据库链接") + @ApiOperation(value = "测试数据库链接") + @PostMapping("/testConnect") + @PreAuthorize("@el.check('database:testConnect')") + public ResponseEntity testConnect(@Validated @RequestBody Database resources){ + return new ResponseEntity<>(databaseService.testConnection(resources),HttpStatus.CREATED); + } + + @Log("执行SQL脚本") + @ApiOperation(value = "执行SQL脚本") + @PostMapping(value = "/upload") + @PreAuthorize("@el.check('database:add')") + public ResponseEntity uploadDatabase(@RequestBody MultipartFile file, HttpServletRequest request)throws Exception{ + String id = request.getParameter("id"); + DatabaseDto database = databaseService.findById(id); + String fileName; + if(database != null){ + fileName = file.getOriginalFilename(); + File executeFile = new File(fileSavePath+fileName); + FileUtil.del(executeFile); + file.transferTo(executeFile); + String result = SqlUtils.executeFile(database.getJdbcUrl(), database.getUserName(), database.getPwd(), executeFile); + return new ResponseEntity<>(result,HttpStatus.OK); + }else{ + throw new BadRequestException("Database not exist"); + } + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/rest/DeployController.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/rest/DeployController.java new file mode 100644 index 0000000..3f13813 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/rest/DeployController.java @@ -0,0 +1,155 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import me.zhengjie.annotation.Log; +import me.zhengjie.modules.mnt.domain.Deploy; +import me.zhengjie.modules.mnt.domain.DeployHistory; +import me.zhengjie.modules.mnt.service.DeployService; +import me.zhengjie.modules.mnt.service.dto.DeployDto; +import me.zhengjie.modules.mnt.service.dto.DeployQueryCriteria; +import me.zhengjie.utils.FileUtil; +import me.zhengjie.utils.PageResult; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@RestController +@Api(tags = "运维:部署管理") +@RequiredArgsConstructor +@RequestMapping("/api/deploy") +public class DeployController { + + private final String fileSavePath = FileUtil.getTmpDirPath()+"/"; + private final DeployService deployService; + + + @ApiOperation("导出部署数据") + @GetMapping(value = "/download") + @PreAuthorize("@el.check('database:list')") + public void exportDeployData(HttpServletResponse response, DeployQueryCriteria criteria) throws IOException { + deployService.download(deployService.queryAll(criteria), response); + } + + @ApiOperation(value = "查询部署") + @GetMapping + @PreAuthorize("@el.check('deploy:list')") + public ResponseEntity> queryDeployData(DeployQueryCriteria criteria, Pageable pageable){ + return new ResponseEntity<>(deployService.queryAll(criteria,pageable),HttpStatus.OK); + } + + @Log("新增部署") + @ApiOperation(value = "新增部署") + @PostMapping + @PreAuthorize("@el.check('deploy:add')") + public ResponseEntity createDeploy(@Validated @RequestBody Deploy resources){ + deployService.create(resources); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @Log("修改部署") + @ApiOperation(value = "修改部署") + @PutMapping + @PreAuthorize("@el.check('deploy:edit')") + public ResponseEntity updateDeploy(@Validated @RequestBody Deploy resources){ + deployService.update(resources); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @Log("删除部署") + @ApiOperation(value = "删除部署") + @DeleteMapping + @PreAuthorize("@el.check('deploy:del')") + public ResponseEntity deleteDeploy(@RequestBody Set ids){ + deployService.delete(ids); + return new ResponseEntity<>(HttpStatus.OK); + } + + @Log("上传文件部署") + @ApiOperation(value = "上传文件部署") + @PostMapping(value = "/upload") + @PreAuthorize("@el.check('deploy:edit')") + public ResponseEntity uploadDeploy(@RequestBody MultipartFile file, HttpServletRequest request)throws Exception{ + Long id = Long.valueOf(request.getParameter("id")); + String fileName = ""; + if(file != null){ + fileName = file.getOriginalFilename(); + File deployFile = new File(fileSavePath+fileName); + FileUtil.del(deployFile); + file.transferTo(deployFile); + //文件下一步要根据文件名字来 + deployService.deploy(fileSavePath+fileName ,id); + }else{ + System.out.println("没有找到相对应的文件"); + } + System.out.println("文件上传的原名称为:"+ Objects.requireNonNull(file).getOriginalFilename()); + Map map = new HashMap<>(2); + map.put("errno",0); + map.put("id",fileName); + return new ResponseEntity<>(map,HttpStatus.OK); + } + @Log("系统还原") + @ApiOperation(value = "系统还原") + @PostMapping(value = "/serverReduction") + @PreAuthorize("@el.check('deploy:edit')") + public ResponseEntity serverReduction(@Validated @RequestBody DeployHistory resources){ + String result = deployService.serverReduction(resources); + return new ResponseEntity<>(result,HttpStatus.OK); + } + @Log("服务运行状态") + @ApiOperation(value = "服务运行状态") + @PostMapping(value = "/serverStatus") + @PreAuthorize("@el.check('deploy:edit')") + public ResponseEntity serverStatus(@Validated @RequestBody Deploy resources){ + String result = deployService.serverStatus(resources); + return new ResponseEntity<>(result,HttpStatus.OK); + } + @Log("启动服务") + @ApiOperation(value = "启动服务") + @PostMapping(value = "/startServer") + @PreAuthorize("@el.check('deploy:edit')") + public ResponseEntity startServer(@Validated @RequestBody Deploy resources){ + String result = deployService.startServer(resources); + return new ResponseEntity<>(result,HttpStatus.OK); + } + @Log("停止服务") + @ApiOperation(value = "停止服务") + @PostMapping(value = "/stopServer") + @PreAuthorize("@el.check('deploy:edit')") + public ResponseEntity stopServer(@Validated @RequestBody Deploy resources){ + String result = deployService.stopServer(resources); + return new ResponseEntity<>(result,HttpStatus.OK); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/rest/DeployHistoryController.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/rest/DeployHistoryController.java new file mode 100644 index 0000000..8d3c902 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/rest/DeployHistoryController.java @@ -0,0 +1,69 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import me.zhengjie.annotation.Log; +import me.zhengjie.modules.mnt.service.DeployHistoryService; +import me.zhengjie.modules.mnt.service.dto.DeployHistoryDto; +import me.zhengjie.modules.mnt.service.dto.DeployHistoryQueryCriteria; +import me.zhengjie.utils.PageResult; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Set; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@RestController +@RequiredArgsConstructor +@Api(tags = "运维:部署历史管理") +@RequestMapping("/api/deployHistory") +public class DeployHistoryController { + + private final DeployHistoryService deployhistoryService; + + @ApiOperation("导出部署历史数据") + @GetMapping(value = "/download") + @PreAuthorize("@el.check('deployHistory:list')") + public void exportDeployHistory(HttpServletResponse response, DeployHistoryQueryCriteria criteria) throws IOException { + deployhistoryService.download(deployhistoryService.queryAll(criteria), response); + } + + @ApiOperation(value = "查询部署历史") + @GetMapping + @PreAuthorize("@el.check('deployHistory:list')") + public ResponseEntity> queryDeployHistory(DeployHistoryQueryCriteria criteria, Pageable pageable){ + return new ResponseEntity<>(deployhistoryService.queryAll(criteria,pageable),HttpStatus.OK); + } + + @Log("删除DeployHistory") + @ApiOperation(value = "删除部署历史") + @DeleteMapping + @PreAuthorize("@el.check('deployHistory:del')") + public ResponseEntity deleteDeployHistory(@RequestBody Set ids){ + deployhistoryService.delete(ids); + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/rest/ServerDeployController.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/rest/ServerDeployController.java new file mode 100644 index 0000000..5ccb0ed --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/rest/ServerDeployController.java @@ -0,0 +1,97 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import me.zhengjie.annotation.Log; +import me.zhengjie.modules.mnt.domain.ServerDeploy; +import me.zhengjie.modules.mnt.service.ServerDeployService; +import me.zhengjie.modules.mnt.service.dto.ServerDeployDto; +import me.zhengjie.modules.mnt.service.dto.ServerDeployQueryCriteria; +import me.zhengjie.utils.PageResult; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Set; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@RestController +@Api(tags = "运维:服务器管理") +@RequiredArgsConstructor +@RequestMapping("/api/serverDeploy") +public class ServerDeployController { + + private final ServerDeployService serverDeployService; + + @ApiOperation("导出服务器数据") + @GetMapping(value = "/download") + @PreAuthorize("@el.check('serverDeploy:list')") + public void exportServerDeploy(HttpServletResponse response, ServerDeployQueryCriteria criteria) throws IOException { + serverDeployService.download(serverDeployService.queryAll(criteria), response); + } + + @ApiOperation(value = "查询服务器") + @GetMapping + @PreAuthorize("@el.check('serverDeploy:list')") + public ResponseEntity> queryServerDeploy(ServerDeployQueryCriteria criteria, Pageable pageable){ + return new ResponseEntity<>(serverDeployService.queryAll(criteria,pageable),HttpStatus.OK); + } + + @Log("新增服务器") + @ApiOperation(value = "新增服务器") + @PostMapping + @PreAuthorize("@el.check('serverDeploy:add')") + public ResponseEntity createServerDeploy(@Validated @RequestBody ServerDeploy resources){ + serverDeployService.create(resources); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @Log("修改服务器") + @ApiOperation(value = "修改服务器") + @PutMapping + @PreAuthorize("@el.check('serverDeploy:edit')") + public ResponseEntity updateServerDeploy(@Validated @RequestBody ServerDeploy resources){ + serverDeployService.update(resources); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @Log("删除服务器") + @ApiOperation(value = "删除Server") + @DeleteMapping + @PreAuthorize("@el.check('serverDeploy:del')") + public ResponseEntity deleteServerDeploy(@RequestBody Set ids){ + serverDeployService.delete(ids); + return new ResponseEntity<>(HttpStatus.OK); + } + + @Log("测试连接服务器") + @ApiOperation(value = "测试连接服务器") + @PostMapping("/testConnect") + @PreAuthorize("@el.check('serverDeploy:add')") + public ResponseEntity testConnectServerDeploy(@Validated @RequestBody ServerDeploy resources){ + return new ResponseEntity<>(serverDeployService.testConnect(resources),HttpStatus.CREATED); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/AppService.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/AppService.java new file mode 100644 index 0000000..9bded4d --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/AppService.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service; + +import me.zhengjie.modules.mnt.domain.App; +import me.zhengjie.modules.mnt.service.dto.AppDto; +import me.zhengjie.modules.mnt.service.dto.AppQueryCriteria; +import me.zhengjie.utils.PageResult; +import org.springframework.data.domain.Pageable; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +public interface AppService { + + /** + * 分页查询 + * @param criteria 条件 + * @param pageable 分页参数 + * @return / + */ + PageResult queryAll(AppQueryCriteria criteria, Pageable pageable); + + /** + * 查询全部数据 + * @param criteria 条件 + * @return / + */ + List queryAll(AppQueryCriteria criteria); + + /** + * 根据ID查询 + * @param id / + * @return / + */ + AppDto findById(Long id); + + /** + * 创建 + * @param resources / + */ + void create(App resources); + + /** + * 编辑 + * @param resources / + */ + void update(App resources); + + /** + * 删除 + * @param ids / + */ + void delete(Set ids); + + /** + * 导出数据 + * @param queryAll / + * @param response / + * @throws IOException / + */ + void download(List queryAll, HttpServletResponse response) throws IOException; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/DatabaseService.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/DatabaseService.java new file mode 100644 index 0000000..904f73c --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/DatabaseService.java @@ -0,0 +1,89 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service; + +import me.zhengjie.modules.mnt.domain.Database; +import me.zhengjie.modules.mnt.service.dto.DatabaseDto; +import me.zhengjie.modules.mnt.service.dto.DatabaseQueryCriteria; +import me.zhengjie.utils.PageResult; +import org.springframework.data.domain.Pageable; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +/** + * @author ZhangHouYing + * @date 2019-08-24 + */ +public interface DatabaseService { + + /** + * 分页查询 + * @param criteria 条件 + * @param pageable 分页参数 + * @return / + */ + PageResult queryAll(DatabaseQueryCriteria criteria, Pageable pageable); + + /** + * 查询全部 + * @param criteria 条件 + * @return / + */ + List queryAll(DatabaseQueryCriteria criteria); + + /** + * 根据ID查询 + * @param id / + * @return / + */ + DatabaseDto findById(String id); + + /** + * 创建 + * @param resources / + */ + void create(Database resources); + + /** + * 编辑 + * @param resources / + */ + void update(Database resources); + + /** + * 删除 + * @param ids / + */ + void delete(Set ids); + + /** + * 测试连接数据库 + * @param resources / + * @return / + */ + boolean testConnection(Database resources); + + /** + * 导出数据 + * @param queryAll / + * @param response / + * @throws IOException e + */ + void download(List queryAll, HttpServletResponse response) throws IOException; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/DeployHistoryService.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/DeployHistoryService.java new file mode 100644 index 0000000..07bcb64 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/DeployHistoryService.java @@ -0,0 +1,75 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service; + +import me.zhengjie.modules.mnt.domain.DeployHistory; +import me.zhengjie.modules.mnt.service.dto.DeployHistoryDto; +import me.zhengjie.modules.mnt.service.dto.DeployHistoryQueryCriteria; +import me.zhengjie.utils.PageResult; +import org.springframework.data.domain.Pageable; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +/** + * @author zhanghouying + */ +public interface DeployHistoryService { + + /** + * 分页查询 + * @param criteria 条件 + * @param pageable 分页参数 + * @return / + */ + PageResult queryAll(DeployHistoryQueryCriteria criteria, Pageable pageable); + + /** + * 查询全部 + * @param criteria 条件 + * @return / + */ + List queryAll(DeployHistoryQueryCriteria criteria); + + /** + * 根据ID查询 + * @param id / + * @return / + */ + DeployHistoryDto findById(String id); + + /** + * 创建 + * @param resources / + */ + void create(DeployHistory resources); + + /** + * 删除 + * @param ids / + */ + void delete(Set ids); + + /** + * 导出数据 + * @param queryAll / + * @param response / + * @throws IOException / + */ + void download(List queryAll, HttpServletResponse response) throws IOException; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/DeployService.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/DeployService.java new file mode 100644 index 0000000..7fb78f5 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/DeployService.java @@ -0,0 +1,117 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service; + +import me.zhengjie.modules.mnt.domain.Deploy; +import me.zhengjie.modules.mnt.domain.DeployHistory; +import me.zhengjie.modules.mnt.service.dto.DeployDto; +import me.zhengjie.modules.mnt.service.dto.DeployQueryCriteria; +import me.zhengjie.utils.PageResult; +import org.springframework.data.domain.Pageable; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +public interface DeployService { + + /** + * 分页查询 + * @param criteria 条件 + * @param pageable 分页参数 + * @return / + */ + PageResult queryAll(DeployQueryCriteria criteria, Pageable pageable); + + /** + * 查询全部数据 + * @param criteria 条件 + * @return / + */ + List queryAll(DeployQueryCriteria criteria); + + /** + * 根据ID查询 + * @param id / + * @return / + */ + DeployDto findById(Long id); + + /** + * 创建 + * @param resources / + */ + void create(Deploy resources); + + + /** + * 编辑 + * @param resources / + */ + void update(Deploy resources); + + /** + * 删除 + * @param ids / + */ + void delete(Set ids); + + /** + * 部署文件到服务器 + * @param fileSavePath 文件路径 + * @param appId 应用ID + */ + void deploy(String fileSavePath, Long appId); + + /** + * 查询部署状态 + * @param resources / + * @return / + */ + String serverStatus(Deploy resources); + /** + * 启动服务 + * @param resources / + * @return / + */ + String startServer(Deploy resources); + /** + * 停止服务 + * @param resources / + * @return / + */ + String stopServer(Deploy resources); + + /** + * 停止服务 + * @param resources / + * @return / + */ + String serverReduction(DeployHistory resources); + + /** + * 导出数据 + * @param queryAll / + * @param response / + * @throws IOException / + */ + void download(List queryAll, HttpServletResponse response) throws IOException; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/ServerDeployService.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/ServerDeployService.java new file mode 100644 index 0000000..6dd343f --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/ServerDeployService.java @@ -0,0 +1,96 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service; + +import me.zhengjie.modules.mnt.domain.ServerDeploy; +import me.zhengjie.modules.mnt.service.dto.ServerDeployDto; +import me.zhengjie.modules.mnt.service.dto.ServerDeployQueryCriteria; +import me.zhengjie.utils.PageResult; +import org.springframework.data.domain.Pageable; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +public interface ServerDeployService { + + /** + * 分页查询 + * @param criteria 条件 + * @param pageable 分页参数 + * @return / + */ + PageResult queryAll(ServerDeployQueryCriteria criteria, Pageable pageable); + + /** + * 查询全部数据 + * @param criteria 条件 + * @return / + */ + List queryAll(ServerDeployQueryCriteria criteria); + + /** + * 根据ID查询 + * @param id / + * @return / + */ + ServerDeployDto findById(Long id); + + /** + * 创建 + * @param resources / + */ + void create(ServerDeploy resources); + + /** + * 编辑 + * @param resources / + */ + void update(ServerDeploy resources); + + /** + * 删除 + * @param ids / + */ + void delete(Set ids); + + /** + * 根据IP查询 + * @param ip / + * @return / + */ + ServerDeployDto findByIp(String ip); + + /** + * 测试登录服务器 + * @param resources / + * @return / + */ + Boolean testConnect(ServerDeploy resources); + + /** + * 导出数据 + * @param queryAll / + * @param response / + * @throws IOException / + */ + void download(List queryAll, HttpServletResponse response) throws IOException; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/AppDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/AppDto.java new file mode 100644 index 0000000..c6fd6f7 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/AppDto.java @@ -0,0 +1,71 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service.dto; + +import lombok.Getter; +import lombok.Setter; +import me.zhengjie.base.BaseDTO; +import java.io.Serializable; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Getter +@Setter +public class AppDto extends BaseDTO implements Serializable { + + /** + * 应用编号 + */ + private Long id; + + /** + * 应用名称 + */ + private String name; + + /** + * 端口 + */ + private Integer port; + + /** + * 上传目录 + */ + private String uploadPath; + + /** + * 部署目录 + */ + private String deployPath; + + /** + * 备份目录 + */ + private String backupPath; + + /** + * 启动脚本 + */ + private String startScript; + + /** + * 部署脚本 + */ + private String deployScript; + +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/AppQueryCriteria.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/AppQueryCriteria.java new file mode 100644 index 0000000..17f358f --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/AppQueryCriteria.java @@ -0,0 +1,38 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service.dto; + +import lombok.Data; +import me.zhengjie.annotation.Query; +import java.sql.Timestamp; +import java.util.List; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Data +public class AppQueryCriteria{ + + /** + * 模糊 + */ + @Query(type = Query.Type.INNER_LIKE) + private String name; + + @Query(type = Query.Type.BETWEEN) + private List createTime; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DatabaseDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DatabaseDto.java new file mode 100644 index 0000000..689b06b --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DatabaseDto.java @@ -0,0 +1,55 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service.dto; + +import lombok.Getter; +import lombok.Setter; +import me.zhengjie.base.BaseDTO; +import java.io.Serializable; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Getter +@Setter +public class DatabaseDto extends BaseDTO implements Serializable { + + /** + * id + */ + private String id; + + /** + * 数据库名称 + */ + private String name; + + /** + * 数据库连接地址 + */ + private String jdbcUrl; + + /** + * 数据库密码 + */ + private String pwd; + + /** + * 用户名 + */ + private String userName; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DatabaseQueryCriteria.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DatabaseQueryCriteria.java new file mode 100644 index 0000000..53d619d --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DatabaseQueryCriteria.java @@ -0,0 +1,44 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service.dto; + +import lombok.Data; +import me.zhengjie.annotation.Query; +import java.sql.Timestamp; +import java.util.List; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Data +public class DatabaseQueryCriteria{ + + /** + * 模糊 + */ + @Query(type = Query.Type.INNER_LIKE) + private String name; + + /** + * 精确 + */ + @Query + private String jdbcUrl; + + @Query(type = Query.Type.BETWEEN) + private List createTime; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DeployDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DeployDto.java new file mode 100644 index 0000000..f3d77b8 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DeployDto.java @@ -0,0 +1,78 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service.dto; + +import cn.hutool.core.collection.CollectionUtil; +import lombok.Getter; +import lombok.Setter; +import me.zhengjie.base.BaseDTO; +import java.io.Serializable; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Getter +@Setter +public class DeployDto extends BaseDTO implements Serializable { + + /** + * 部署编号 + */ + private String id; + + private AppDto app; + + /** + * 服务器 + */ + private Set deploys; + + private String servers; + + /** + * 服务状态 + */ + private String status; + + public String getServers() { + if(CollectionUtil.isNotEmpty(deploys)){ + return deploys.stream().map(ServerDeployDto::getName).collect(Collectors.joining(",")); + } + return servers; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DeployDto deployDto = (DeployDto) o; + return Objects.equals(id, deployDto.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DeployHistoryDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DeployHistoryDto.java new file mode 100644 index 0000000..a9f480c --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DeployHistoryDto.java @@ -0,0 +1,58 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service.dto; + +import lombok.Data; +import java.io.Serializable; +import java.sql.Timestamp; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Data +public class DeployHistoryDto implements Serializable { + + /** + * 编号 + */ + private String id; + + /** + * 应用名称 + */ + private String appName; + + /** + * 部署IP + */ + private String ip; + + /** + * 部署时间 + */ + private Timestamp deployDate; + + /** + * 部署人员 + */ + private String deployUser; + + /** + * 部署编号 + */ + private Long deployId; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DeployHistoryQueryCriteria.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DeployHistoryQueryCriteria.java new file mode 100644 index 0000000..c34f124 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DeployHistoryQueryCriteria.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service.dto; + +import lombok.Data; +import me.zhengjie.annotation.Query; +import java.sql.Timestamp; +import java.util.List; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Data +public class DeployHistoryQueryCriteria{ + + /** + * 精确 + */ + @Query(blurry = "appName,ip,deployUser") + private String blurry; + + @Query + private Long deployId; + + @Query(type = Query.Type.BETWEEN) + private List deployDate; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DeployQueryCriteria.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DeployQueryCriteria.java new file mode 100644 index 0000000..c404620 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/DeployQueryCriteria.java @@ -0,0 +1,39 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service.dto; + +import lombok.Data; +import me.zhengjie.annotation.Query; +import java.sql.Timestamp; +import java.util.List; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Data +public class DeployQueryCriteria{ + + /** + * 模糊 + */ + @Query(type = Query.Type.INNER_LIKE, propName = "name", joinName = "app") + private String appName; + + @Query(type = Query.Type.BETWEEN) + private List createTime; + +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/ServerDeployDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/ServerDeployDto.java new file mode 100644 index 0000000..a49c795 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/ServerDeployDto.java @@ -0,0 +1,61 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service.dto; + +import lombok.Getter; +import lombok.Setter; +import me.zhengjie.base.BaseDTO; +import java.io.Serializable; +import java.util.Objects; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Getter +@Setter +public class ServerDeployDto extends BaseDTO implements Serializable { + + private Long id; + + private String name; + + private String ip; + + private Integer port; + + private String account; + + private String password; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ServerDeployDto that = (ServerDeployDto) o; + return Objects.equals(id, that.id) && + Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/ServerDeployQueryCriteria.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/ServerDeployQueryCriteria.java new file mode 100644 index 0000000..bb8bd41 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/dto/ServerDeployQueryCriteria.java @@ -0,0 +1,38 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service.dto; + +import lombok.Data; +import me.zhengjie.annotation.Query; +import java.sql.Timestamp; +import java.util.List; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Data +public class ServerDeployQueryCriteria{ + + /** + * 模糊 + */ + @Query(blurry = "name,ip,account") + private String blurry; + + @Query(type = Query.Type.BETWEEN) + private List createTime; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/impl/AppServiceImpl.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/impl/AppServiceImpl.java new file mode 100644 index 0000000..7d66d0b --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/impl/AppServiceImpl.java @@ -0,0 +1,120 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service.impl; + +import lombok.RequiredArgsConstructor; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.modules.mnt.domain.App; +import me.zhengjie.modules.mnt.repository.AppRepository; +import me.zhengjie.modules.mnt.service.AppService; +import me.zhengjie.modules.mnt.service.dto.AppDto; +import me.zhengjie.modules.mnt.service.dto.AppQueryCriteria; +import me.zhengjie.modules.mnt.service.mapstruct.AppMapper; +import me.zhengjie.utils.*; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Service +@RequiredArgsConstructor +public class AppServiceImpl implements AppService { + + private final AppRepository appRepository; + private final AppMapper appMapper; + + @Override + public PageResult queryAll(AppQueryCriteria criteria, Pageable pageable){ + Page page = appRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder),pageable); + return PageUtil.toPage(page.map(appMapper::toDto)); + } + + @Override + public List queryAll(AppQueryCriteria criteria){ + return appMapper.toDto(appRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder))); + } + + @Override + public AppDto findById(Long id) { + App app = appRepository.findById(id).orElseGet(App::new); + ValidationUtil.isNull(app.getId(),"App","id",id); + return appMapper.toDto(app); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void create(App resources) { + verification(resources); + appRepository.save(resources); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(App resources) { + verification(resources); + App app = appRepository.findById(resources.getId()).orElseGet(App::new); + ValidationUtil.isNull(app.getId(),"App","id",resources.getId()); + app.copy(resources); + appRepository.save(app); + } + + private void verification(App resources){ + String opt = "/opt"; + String home = "/home"; + if (!(resources.getUploadPath().startsWith(opt) || resources.getUploadPath().startsWith(home))) { + throw new BadRequestException("文件只能上传在opt目录或者home目录 "); + } + if (!(resources.getDeployPath().startsWith(opt) || resources.getDeployPath().startsWith(home))) { + throw new BadRequestException("文件只能部署在opt目录或者home目录 "); + } + if (!(resources.getBackupPath().startsWith(opt) || resources.getBackupPath().startsWith(home))) { + throw new BadRequestException("文件只能备份在opt目录或者home目录 "); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Set ids) { + for (Long id : ids) { + appRepository.deleteById(id); + } + } + + @Override + public void download(List queryAll, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (AppDto appDto : queryAll) { + Map map = new LinkedHashMap<>(); + map.put("应用名称", appDto.getName()); + map.put("端口", appDto.getPort()); + map.put("上传目录", appDto.getUploadPath()); + map.put("部署目录", appDto.getDeployPath()); + map.put("备份目录", appDto.getBackupPath()); + map.put("启动脚本", appDto.getStartScript()); + map.put("部署脚本", appDto.getDeployScript()); + map.put("创建日期", appDto.getCreateTime()); + list.add(map); + } + FileUtil.downloadExcel(list, response); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/impl/DatabaseServiceImpl.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/impl/DatabaseServiceImpl.java new file mode 100644 index 0000000..fa55782 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/impl/DatabaseServiceImpl.java @@ -0,0 +1,114 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service.impl; + +import cn.hutool.core.util.IdUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.zhengjie.modules.mnt.domain.Database; +import me.zhengjie.modules.mnt.repository.DatabaseRepository; +import me.zhengjie.modules.mnt.service.DatabaseService; +import me.zhengjie.modules.mnt.service.dto.DatabaseDto; +import me.zhengjie.modules.mnt.service.dto.DatabaseQueryCriteria; +import me.zhengjie.modules.mnt.service.mapstruct.DatabaseMapper; +import me.zhengjie.modules.mnt.util.SqlUtils; +import me.zhengjie.utils.*; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Slf4j +@Service +@RequiredArgsConstructor +public class DatabaseServiceImpl implements DatabaseService { + + private final DatabaseRepository databaseRepository; + private final DatabaseMapper databaseMapper; + + @Override + public PageResult queryAll(DatabaseQueryCriteria criteria, Pageable pageable){ + Page page = databaseRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder),pageable); + return PageUtil.toPage(page.map(databaseMapper::toDto)); + } + + @Override + public List queryAll(DatabaseQueryCriteria criteria){ + return databaseMapper.toDto(databaseRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder))); + } + + @Override + public DatabaseDto findById(String id) { + Database database = databaseRepository.findById(id).orElseGet(Database::new); + ValidationUtil.isNull(database.getId(),"Database","id",id); + return databaseMapper.toDto(database); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void create(Database resources) { + resources.setId(IdUtil.simpleUUID()); + databaseRepository.save(resources); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(Database resources) { + Database database = databaseRepository.findById(resources.getId()).orElseGet(Database::new); + ValidationUtil.isNull(database.getId(),"Database","id",resources.getId()); + database.copy(resources); + databaseRepository.save(database); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Set ids) { + for (String id : ids) { + databaseRepository.deleteById(id); + } + } + + @Override + public boolean testConnection(Database resources) { + try { + return SqlUtils.testConnection(resources.getJdbcUrl(), resources.getUserName(), resources.getPwd()); + } catch (Exception e) { + log.error(e.getMessage()); + return false; + } + } + + @Override + public void download(List queryAll, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (DatabaseDto databaseDto : queryAll) { + Map map = new LinkedHashMap<>(); + map.put("数据库名称", databaseDto.getName()); + map.put("数据库连接地址", databaseDto.getJdbcUrl()); + map.put("用户名", databaseDto.getUserName()); + map.put("创建日期", databaseDto.getCreateTime()); + list.add(map); + } + FileUtil.downloadExcel(list, response); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/impl/DeployHistoryServiceImpl.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/impl/DeployHistoryServiceImpl.java new file mode 100644 index 0000000..762b783 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/impl/DeployHistoryServiceImpl.java @@ -0,0 +1,93 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service.impl; + +import cn.hutool.core.util.IdUtil; +import lombok.RequiredArgsConstructor; +import me.zhengjie.modules.mnt.domain.DeployHistory; +import me.zhengjie.modules.mnt.repository.DeployHistoryRepository; +import me.zhengjie.modules.mnt.service.DeployHistoryService; +import me.zhengjie.modules.mnt.service.dto.DeployHistoryDto; +import me.zhengjie.modules.mnt.service.dto.DeployHistoryQueryCriteria; +import me.zhengjie.modules.mnt.service.mapstruct.DeployHistoryMapper; +import me.zhengjie.utils.*; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Service +@RequiredArgsConstructor +public class DeployHistoryServiceImpl implements DeployHistoryService { + + private final DeployHistoryRepository deployhistoryRepository; + private final DeployHistoryMapper deployhistoryMapper; + + @Override + public PageResult queryAll(DeployHistoryQueryCriteria criteria, Pageable pageable){ + Page page = deployhistoryRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder),pageable); + return PageUtil.toPage(page.map(deployhistoryMapper::toDto)); + } + + @Override + public List queryAll(DeployHistoryQueryCriteria criteria){ + return deployhistoryMapper.toDto(deployhistoryRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder))); + } + + @Override + public DeployHistoryDto findById(String id) { + DeployHistory deployhistory = deployhistoryRepository.findById(id).orElseGet(DeployHistory::new); + ValidationUtil.isNull(deployhistory.getId(),"DeployHistory","id",id); + return deployhistoryMapper.toDto(deployhistory); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void create(DeployHistory resources) { + resources.setId(IdUtil.simpleUUID()); + deployhistoryRepository.save(resources); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Set ids) { + for (String id : ids) { + deployhistoryRepository.deleteById(id); + } + } + + @Override + public void download(List queryAll, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (DeployHistoryDto deployHistoryDto : queryAll) { + Map map = new LinkedHashMap<>(); + map.put("部署编号", deployHistoryDto.getDeployId()); + map.put("应用名称", deployHistoryDto.getAppName()); + map.put("部署IP", deployHistoryDto.getIp()); + map.put("部署时间", deployHistoryDto.getDeployDate()); + map.put("部署人员", deployHistoryDto.getDeployUser()); + list.add(map); + } + FileUtil.downloadExcel(list, response); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/impl/DeployServiceImpl.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/impl/DeployServiceImpl.java new file mode 100644 index 0000000..1c16f9c --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/impl/DeployServiceImpl.java @@ -0,0 +1,430 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service.impl; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.modules.mnt.domain.App; +import me.zhengjie.modules.mnt.domain.Deploy; +import me.zhengjie.modules.mnt.domain.DeployHistory; +import me.zhengjie.modules.mnt.domain.ServerDeploy; +import me.zhengjie.modules.mnt.repository.DeployRepository; +import me.zhengjie.modules.mnt.service.DeployHistoryService; +import me.zhengjie.modules.mnt.service.DeployService; +import me.zhengjie.modules.mnt.service.ServerDeployService; +import me.zhengjie.modules.mnt.service.dto.AppDto; +import me.zhengjie.modules.mnt.service.dto.DeployDto; +import me.zhengjie.modules.mnt.service.dto.DeployQueryCriteria; +import me.zhengjie.modules.mnt.service.dto.ServerDeployDto; +import me.zhengjie.modules.mnt.service.mapstruct.DeployMapper; +import me.zhengjie.modules.mnt.util.ExecuteShellUtil; +import me.zhengjie.modules.mnt.util.ScpClientUtil; +import me.zhengjie.modules.mnt.websocket.MsgType; +import me.zhengjie.modules.mnt.websocket.SocketMsg; +import me.zhengjie.modules.mnt.websocket.WebSocketServer; +import me.zhengjie.utils.*; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; + +/** + * @author zhanghouying + * @date 2019-08-24 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class DeployServiceImpl implements DeployService { + + private final String FILE_SEPARATOR = "/"; + private final DeployRepository deployRepository; + private final DeployMapper deployMapper; + private final ServerDeployService serverDeployService; + private final DeployHistoryService deployHistoryService; + /** + * 循环次数 + */ + private final Integer count = 30; + + + @Override + public PageResult queryAll(DeployQueryCriteria criteria, Pageable pageable) { + Page page = deployRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root, criteria, criteriaBuilder), pageable); + return PageUtil.toPage(page.map(deployMapper::toDto)); + } + + @Override + public List queryAll(DeployQueryCriteria criteria) { + return deployMapper.toDto(deployRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root, criteria, criteriaBuilder))); + } + + @Override + public DeployDto findById(Long id) { + Deploy deploy = deployRepository.findById(id).orElseGet(Deploy::new); + ValidationUtil.isNull(deploy.getId(), "Deploy", "id", id); + return deployMapper.toDto(deploy); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void create(Deploy resources) { + deployRepository.save(resources); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(Deploy resources) { + Deploy deploy = deployRepository.findById(resources.getId()).orElseGet(Deploy::new); + ValidationUtil.isNull(deploy.getId(), "Deploy", "id", resources.getId()); + deploy.copy(resources); + deployRepository.save(deploy); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Set ids) { + for (Long id : ids) { + deployRepository.deleteById(id); + } + } + + @Override + public void deploy(String fileSavePath, Long id) { + deployApp(fileSavePath, id); + } + + /** + * @param fileSavePath 本机路径 + * @param id ID + */ + private void deployApp(String fileSavePath, Long id) { + + DeployDto deploy = findById(id); + if (deploy == null) { + sendMsg("部署信息不存在", MsgType.ERROR); + throw new BadRequestException("部署信息不存在"); + } + AppDto app = deploy.getApp(); + if (app == null) { + sendMsg("包对应应用信息不存在", MsgType.ERROR); + throw new BadRequestException("包对应应用信息不存在"); + } + int port = app.getPort(); + //这个是服务器部署路径 + String uploadPath = app.getUploadPath(); + StringBuilder sb = new StringBuilder(); + String msg; + Set deploys = deploy.getDeploys(); + for (ServerDeployDto deployDTO : deploys) { + String ip = deployDTO.getIp(); + ExecuteShellUtil executeShellUtil = getExecuteShellUtil(ip); + //判断是否第一次部署 + boolean flag = checkFile(executeShellUtil, app); + //第一步要确认服务器上有这个目录 + executeShellUtil.execute("mkdir -p " + app.getUploadPath()); + executeShellUtil.execute("mkdir -p " + app.getBackupPath()); + executeShellUtil.execute("mkdir -p " + app.getDeployPath()); + //上传文件 + msg = String.format("登陆到服务器:%s", ip); + ScpClientUtil scpClientUtil = getScpClientUtil(ip); + log.info(msg); + sendMsg(msg, MsgType.INFO); + msg = String.format("上传文件到服务器:%s
目录:%s下,请稍等...", ip, uploadPath); + sendMsg(msg, MsgType.INFO); + scpClientUtil.putFile(fileSavePath, uploadPath); + if (flag) { + sendMsg("停止原来应用", MsgType.INFO); + //停止应用 + stopApp(port, executeShellUtil); + sendMsg("备份原来应用", MsgType.INFO); + //备份应用 + backupApp(executeShellUtil, ip, app.getDeployPath()+FILE_SEPARATOR, app.getName(), app.getBackupPath()+FILE_SEPARATOR, id); + } + sendMsg("部署应用", MsgType.INFO); + //部署文件,并启动应用 + String deployScript = app.getDeployScript(); + executeShellUtil.execute(deployScript); + sleep(3); + sendMsg("应用部署中,请耐心等待部署结果,或者稍后手动查看部署状态", MsgType.INFO); + int i = 0; + boolean result = false; + // 由于启动应用需要时间,所以需要循环获取状态,如果超过30次,则认为是启动失败 + while (i++ < count){ + result = checkIsRunningStatus(port, executeShellUtil); + if(result){ + break; + } + // 休眠6秒 + sleep(6); + } + sb.append("服务器:").append(deployDTO.getName()).append("
应用:").append(app.getName()); + sendResultMsg(result, sb); + executeShellUtil.close(); + } + } + + private void sleep(int second) { + try { + Thread.sleep(second * 1000); + } catch (InterruptedException e) { + log.error(e.getMessage(),e); + } + } + + private void backupApp(ExecuteShellUtil executeShellUtil, String ip, String fileSavePath, String appName, String backupPath, Long id) { + String deployDate = DateUtil.format(new Date(), DatePattern.PURE_DATETIME_PATTERN); + StringBuilder sb = new StringBuilder(); + backupPath += appName + FILE_SEPARATOR + deployDate + "\n"; + sb.append("mkdir -p ").append(backupPath); + sb.append("mv -f ").append(fileSavePath); + sb.append(appName).append(" ").append(backupPath); + log.info("备份应用脚本:" + sb.toString()); + executeShellUtil.execute(sb.toString()); + //还原信息入库 + DeployHistory deployHistory = new DeployHistory(); + deployHistory.setAppName(appName); + deployHistory.setDeployUser(SecurityUtils.getCurrentUsername()); + deployHistory.setIp(ip); + deployHistory.setDeployId(id); + deployHistoryService.create(deployHistory); + } + + /** + * 停App + * + * @param port 端口 + * @param executeShellUtil / + */ + private void stopApp(int port, ExecuteShellUtil executeShellUtil) { + //发送停止命令 + executeShellUtil.execute(String.format("lsof -i :%d|grep -v \"PID\"|awk '{print \"kill -9\",$2}'|sh", port)); + + } + + /** + * 指定端口程序是否在运行 + * + * @param port 端口 + * @param executeShellUtil / + * @return true 正在运行 false 已经停止 + */ + private boolean checkIsRunningStatus(int port, ExecuteShellUtil executeShellUtil) { + String result = executeShellUtil.executeForResult(String.format("fuser -n tcp %d", port)); + return result.indexOf("/tcp:")>0; + } + + private void sendMsg(String msg, MsgType msgType) { + try { + WebSocketServer.sendInfo(new SocketMsg(msg, msgType), "deploy"); + } catch (IOException e) { + log.error(e.getMessage(),e); + } + } + + @Override + public String serverStatus(Deploy resources) { + Set serverDeploys = resources.getDeploys(); + App app = resources.getApp(); + for (ServerDeploy serverDeploy : serverDeploys) { + StringBuilder sb = new StringBuilder(); + ExecuteShellUtil executeShellUtil = getExecuteShellUtil(serverDeploy.getIp()); + sb.append("服务器:").append(serverDeploy.getName()).append("
应用:").append(app.getName()); + boolean result = checkIsRunningStatus(app.getPort(), executeShellUtil); + if (result) { + sb.append("
正在运行"); + sendMsg(sb.toString(), MsgType.INFO); + } else { + sb.append("
已停止!"); + sendMsg(sb.toString(), MsgType.ERROR); + } + log.info(sb.toString()); + executeShellUtil.close(); + } + return "执行完毕"; + } + + private boolean checkFile(ExecuteShellUtil executeShellUtil, AppDto appDTO) { + String result = executeShellUtil.executeForResult("find " + appDTO.getDeployPath() + " -name " + appDTO.getName()); + return result.indexOf(appDTO.getName())>0; + } + + /** + * 启动服务 + * @param resources / + * @return / + */ + @Override + public String startServer(Deploy resources) { + Set deploys = resources.getDeploys(); + App app = resources.getApp(); + for (ServerDeploy deploy : deploys) { + StringBuilder sb = new StringBuilder(); + ExecuteShellUtil executeShellUtil = getExecuteShellUtil(deploy.getIp()); + //为了防止重复启动,这里先停止应用 + stopApp(app.getPort(), executeShellUtil); + sb.append("服务器:").append(deploy.getName()).append("
应用:").append(app.getName()); + sendMsg("下发启动命令", MsgType.INFO); + executeShellUtil.execute(app.getStartScript()); + sleep(3); + sendMsg("应用启动中,请耐心等待启动结果,或者稍后手动查看运行状态", MsgType.INFO); + int i = 0; + boolean result = false; + // 由于启动应用需要时间,所以需要循环获取状态,如果超过30次,则认为是启动失败 + while (i++ < count){ + result = checkIsRunningStatus(app.getPort(), executeShellUtil); + if(result){ + break; + } + // 休眠6秒 + sleep(6); + } + sendResultMsg(result, sb); + log.info(sb.toString()); + executeShellUtil.close(); + } + return "执行完毕"; + } + + /** + * 停止服务 + * @param resources / + * @return / + */ + @Override + public String stopServer(Deploy resources) { + Set deploys = resources.getDeploys(); + App app = resources.getApp(); + for (ServerDeploy deploy : deploys) { + StringBuilder sb = new StringBuilder(); + ExecuteShellUtil executeShellUtil = getExecuteShellUtil(deploy.getIp()); + sb.append("服务器:").append(deploy.getName()).append("
应用:").append(app.getName()); + sendMsg("下发停止命令", MsgType.INFO); + //停止应用 + stopApp(app.getPort(), executeShellUtil); + sleep(1); + boolean result = checkIsRunningStatus(app.getPort(), executeShellUtil); + if (result) { + sb.append("
关闭失败!"); + sendMsg(sb.toString(), MsgType.ERROR); + } else { + sb.append("
关闭成功!"); + sendMsg(sb.toString(), MsgType.INFO); + } + log.info(sb.toString()); + executeShellUtil.close(); + } + return "执行完毕"; + } + + @Override + public String serverReduction(DeployHistory resources) { + Long deployId = resources.getDeployId(); + Deploy deployInfo = deployRepository.findById(deployId).orElseGet(Deploy::new); + String deployDate = DateUtil.format(resources.getDeployDate(), DatePattern.PURE_DATETIME_PATTERN); + App app = deployInfo.getApp(); + if (app == null) { + sendMsg("应用信息不存在:" + resources.getAppName(), MsgType.ERROR); + throw new BadRequestException("应用信息不存在:" + resources.getAppName()); + } + String backupPath = app.getBackupPath()+FILE_SEPARATOR; + backupPath += resources.getAppName() + FILE_SEPARATOR + deployDate; + //这个是服务器部署路径 + String deployPath = app.getDeployPath(); + String ip = resources.getIp(); + ExecuteShellUtil executeShellUtil = getExecuteShellUtil(ip); + String msg; + + msg = String.format("登陆到服务器:%s", ip); + log.info(msg); + sendMsg(msg, MsgType.INFO); + sendMsg("停止原来应用", MsgType.INFO); + //停止应用 + stopApp(app.getPort(), executeShellUtil); + //删除原来应用 + sendMsg("删除应用", MsgType.INFO); + executeShellUtil.execute("rm -rf " + deployPath + FILE_SEPARATOR + resources.getAppName()); + //还原应用 + sendMsg("还原应用", MsgType.INFO); + executeShellUtil.execute("cp -r " + backupPath + "/. " + deployPath); + sendMsg("启动应用", MsgType.INFO); + executeShellUtil.execute(app.getStartScript()); + sendMsg("应用启动中,请耐心等待启动结果,或者稍后手动查看启动状态", MsgType.INFO); + int i = 0; + boolean result = false; + // 由于启动应用需要时间,所以需要循环获取状态,如果超过30次,则认为是启动失败 + while (i++ < count){ + result = checkIsRunningStatus(app.getPort(), executeShellUtil); + if(result){ + break; + } + // 休眠6秒 + sleep(6); + } + StringBuilder sb = new StringBuilder(); + sb.append("服务器:").append(ip).append("
应用:").append(resources.getAppName()); + sendResultMsg(result, sb); + executeShellUtil.close(); + return ""; + } + + private ExecuteShellUtil getExecuteShellUtil(String ip) { + ServerDeployDto serverDeployDTO = serverDeployService.findByIp(ip); + if (serverDeployDTO == null) { + sendMsg("IP对应服务器信息不存在:" + ip, MsgType.ERROR); + throw new BadRequestException("IP对应服务器信息不存在:" + ip); + } + return new ExecuteShellUtil(ip, serverDeployDTO.getAccount(), serverDeployDTO.getPassword(),serverDeployDTO.getPort()); + } + + private ScpClientUtil getScpClientUtil(String ip) { + ServerDeployDto serverDeployDTO = serverDeployService.findByIp(ip); + if (serverDeployDTO == null) { + sendMsg("IP对应服务器信息不存在:" + ip, MsgType.ERROR); + throw new BadRequestException("IP对应服务器信息不存在:" + ip); + } + return ScpClientUtil.getInstance(ip, serverDeployDTO.getPort(), serverDeployDTO.getAccount(), serverDeployDTO.getPassword()); + } + + private void sendResultMsg(boolean result, StringBuilder sb) { + if (result) { + sb.append("
启动成功!"); + sendMsg(sb.toString(), MsgType.INFO); + } else { + sb.append("
启动失败!"); + sendMsg(sb.toString(), MsgType.ERROR); + } + } + + @Override + public void download(List queryAll, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (DeployDto deployDto : queryAll) { + Map map = new LinkedHashMap<>(); + map.put("应用名称", deployDto.getApp().getName()); + map.put("服务器", deployDto.getServers()); + map.put("部署日期", deployDto.getCreateTime()); + list.add(map); + } + FileUtil.downloadExcel(list, response); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/impl/ServerDeployServiceImpl.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/impl/ServerDeployServiceImpl.java new file mode 100644 index 0000000..9e25eb4 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/impl/ServerDeployServiceImpl.java @@ -0,0 +1,122 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service.impl; + +import lombok.RequiredArgsConstructor; +import me.zhengjie.modules.mnt.domain.ServerDeploy; +import me.zhengjie.modules.mnt.repository.ServerDeployRepository; +import me.zhengjie.modules.mnt.service.ServerDeployService; +import me.zhengjie.modules.mnt.service.dto.ServerDeployDto; +import me.zhengjie.modules.mnt.service.dto.ServerDeployQueryCriteria; +import me.zhengjie.modules.mnt.service.mapstruct.ServerDeployMapper; +import me.zhengjie.modules.mnt.util.ExecuteShellUtil; +import me.zhengjie.utils.*; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Service +@RequiredArgsConstructor +public class ServerDeployServiceImpl implements ServerDeployService { + + private final ServerDeployRepository serverDeployRepository; + private final ServerDeployMapper serverDeployMapper; + + @Override + public PageResult queryAll(ServerDeployQueryCriteria criteria, Pageable pageable){ + Page page = serverDeployRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder),pageable); + return PageUtil.toPage(page.map(serverDeployMapper::toDto)); + } + + @Override + public List queryAll(ServerDeployQueryCriteria criteria){ + return serverDeployMapper.toDto(serverDeployRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder))); + } + + @Override + public ServerDeployDto findById(Long id) { + ServerDeploy server = serverDeployRepository.findById(id).orElseGet(ServerDeploy::new); + ValidationUtil.isNull(server.getId(),"ServerDeploy","id",id); + return serverDeployMapper.toDto(server); + } + + @Override + public ServerDeployDto findByIp(String ip) { + ServerDeploy deploy = serverDeployRepository.findByIp(ip); + return serverDeployMapper.toDto(deploy); + } + + @Override + public Boolean testConnect(ServerDeploy resources) { + ExecuteShellUtil executeShellUtil = null; + try { + executeShellUtil = new ExecuteShellUtil(resources.getIp(), resources.getAccount(), resources.getPassword(),resources.getPort()); + return executeShellUtil.execute("ls")==0; + } catch (Exception e) { + return false; + }finally { + if (executeShellUtil != null) { + executeShellUtil.close(); + } + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void create(ServerDeploy resources) { + serverDeployRepository.save(resources); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(ServerDeploy resources) { + ServerDeploy serverDeploy = serverDeployRepository.findById(resources.getId()).orElseGet(ServerDeploy::new); + ValidationUtil.isNull( serverDeploy.getId(),"ServerDeploy","id",resources.getId()); + serverDeploy.copy(resources); + serverDeployRepository.save(serverDeploy); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Set ids) { + for (Long id : ids) { + serverDeployRepository.deleteById(id); + } + } + + @Override + public void download(List queryAll, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (ServerDeployDto deployDto : queryAll) { + Map map = new LinkedHashMap<>(); + map.put("服务器名称", deployDto.getName()); + map.put("服务器IP", deployDto.getIp()); + map.put("端口", deployDto.getPort()); + map.put("账号", deployDto.getAccount()); + map.put("创建日期", deployDto.getCreateTime()); + list.add(map); + } + FileUtil.downloadExcel(list, response); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/mapstruct/AppMapper.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/mapstruct/AppMapper.java new file mode 100644 index 0000000..fc39eeb --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/mapstruct/AppMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import me.zhengjie.modules.mnt.domain.App; +import me.zhengjie.modules.mnt.service.dto.AppDto; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Mapper(componentModel = "spring",uses = {},unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface AppMapper extends BaseMapper { + +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/mapstruct/DatabaseMapper.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/mapstruct/DatabaseMapper.java new file mode 100644 index 0000000..3cc6e8d --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/mapstruct/DatabaseMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import me.zhengjie.modules.mnt.domain.Database; +import me.zhengjie.modules.mnt.service.dto.DatabaseDto; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Mapper(componentModel = "spring",unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface DatabaseMapper extends BaseMapper { + +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/mapstruct/DeployHistoryMapper.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/mapstruct/DeployHistoryMapper.java new file mode 100644 index 0000000..2522ab0 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/mapstruct/DeployHistoryMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import me.zhengjie.modules.mnt.domain.DeployHistory; +import me.zhengjie.modules.mnt.service.dto.DeployHistoryDto; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Mapper(componentModel = "spring",uses = {},unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface DeployHistoryMapper extends BaseMapper { + +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/mapstruct/DeployMapper.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/mapstruct/DeployMapper.java new file mode 100644 index 0000000..cd3edee --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/mapstruct/DeployMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import me.zhengjie.modules.mnt.domain.Deploy; +import me.zhengjie.modules.mnt.service.dto.DeployDto; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Mapper(componentModel = "spring",uses = {AppMapper.class, ServerDeployMapper.class},unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface DeployMapper extends BaseMapper { + +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/mapstruct/ServerDeployMapper.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/mapstruct/ServerDeployMapper.java new file mode 100644 index 0000000..960b25b --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/service/mapstruct/ServerDeployMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import me.zhengjie.modules.mnt.domain.ServerDeploy; +import me.zhengjie.modules.mnt.service.dto.ServerDeployDto; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** +* @author zhanghouying +* @date 2019-08-24 +*/ +@Mapper(componentModel = "spring",uses = {},unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface ServerDeployMapper extends BaseMapper { + +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/util/DataTypeEnum.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/util/DataTypeEnum.java new file mode 100644 index 0000000..e104b9e --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/util/DataTypeEnum.java @@ -0,0 +1,140 @@ +/* + * << + * Davinci + * == + * Copyright (C) 2016 - 2019 EDP + * == + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * >> + * + */ + +package me.zhengjie.modules.mnt.util; +import lombok.extern.slf4j.Slf4j; + +/** + * @author / + */ +@Slf4j +@SuppressWarnings({"unchecked","all"}) +public enum DataTypeEnum { + + /** mysql */ + MYSQL("mysql", "mysql", "com.mysql.jdbc.Driver", "`", "`", "'", "'"), + + /** oracle */ + ORACLE("oracle", "oracle", "oracle.jdbc.driver.OracleDriver", "\"", "\"", "\"", "\""), + + /** sql server */ + SQLSERVER("sqlserver", "sqlserver", "com.microsoft.sqlserver.jdbc.SQLServerDriver", "\"", "\"", "\"", "\""), + + /** h2 */ + H2("h2", "h2", "org.h2.Driver", "`", "`", "\"", "\""), + + /** phoenix */ + PHOENIX("phoenix", "hbase phoenix", "org.apache.phoenix.jdbc.PhoenixDriver", "", "", "\"", "\""), + + /** mongo */ + MONGODB("mongo", "mongodb", "mongodb.jdbc.MongoDriver", "`", "`", "\"", "\""), + + /** sql4es */ + ELASTICSEARCH("sql4es", "elasticsearch", "nl.anchormen.sql4es.jdbc.ESDriver", "", "", "'", "'"), + + /** presto */ + PRESTO("presto", "presto", "com.facebook.presto.jdbc.PrestoDriver", "", "", "\"", "\""), + + /** moonbox */ + MOONBOX("moonbox", "moonbox", "moonbox.jdbc.MbDriver", "`", "`", "`", "`"), + + /** cassandra */ + CASSANDRA("cassandra", "cassandra", "com.github.adejanovski.cassandra.jdbc.CassandraDriver", "", "", "'", "'"), + + /** click house */ + CLICKHOUSE("clickhouse", "clickhouse", "ru.yandex.clickhouse.ClickHouseDriver", "", "", "\"", "\""), + + /** kylin */ + KYLIN("kylin", "kylin", "org.apache.kylin.jdbc.Driver", "\"", "\"", "\"", "\""), + + /** vertica */ + VERTICA("vertica", "vertica", "com.vertica.jdbc.Driver", "", "", "'", "'"), + + /** sap */ + HANA("sap", "sap hana", "com.sap.db.jdbc.Driver", "", "", "'", "'"), + + /** impala */ + IMPALA("impala", "impala", "com.cloudera.impala.jdbc41.Driver", "", "", "'", "'"); + + private String feature; + private String desc; + private String driver; + private String keywordPrefix; + private String keywordSuffix; + private String aliasPrefix; + private String aliasSuffix; + + private static final String JDBC_URL_PREFIX = "jdbc:"; + + DataTypeEnum(String feature, String desc, String driver, String keywordPrefix, String keywordSuffix, String aliasPrefix, String aliasSuffix) { + this.feature = feature; + this.desc = desc; + this.driver = driver; + this.keywordPrefix = keywordPrefix; + this.keywordSuffix = keywordSuffix; + this.aliasPrefix = aliasPrefix; + this.aliasSuffix = aliasSuffix; + } + + public static DataTypeEnum urlOf(String jdbcUrl) { + String url = jdbcUrl.toLowerCase().trim(); + for (DataTypeEnum dataTypeEnum : values()) { + if (url.startsWith(JDBC_URL_PREFIX + dataTypeEnum.feature)) { + try { + Class aClass = Class.forName(dataTypeEnum.getDriver()); + if (null == aClass) { + throw new RuntimeException("Unable to get driver instance for jdbcUrl: " + jdbcUrl); + } + } catch (ClassNotFoundException e) { + throw new RuntimeException("Unable to get driver instance: " + jdbcUrl); + } + return dataTypeEnum; + } + } + return null; + } + + public String getFeature() { + return feature; + } + + public String getDesc() { + return desc; + } + + public String getDriver() { + return driver; + } + + public String getKeywordPrefix() { + return keywordPrefix; + } + + public String getKeywordSuffix() { + return keywordSuffix; + } + + public String getAliasPrefix() { + return aliasPrefix; + } + + public String getAliasSuffix() { + return aliasSuffix; + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/util/ExecuteShellUtil.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/util/ExecuteShellUtil.java new file mode 100644 index 0000000..a5d5b59 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/util/ExecuteShellUtil.java @@ -0,0 +1,101 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.util; + +import cn.hutool.core.io.IoUtil; +import com.jcraft.jsch.ChannelShell; +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.Session; +import lombok.extern.slf4j.Slf4j; + +import java.io.*; +import java.util.Vector; + +/** + * 执行shell命令 + * + * @author: ZhangHouYing + * @date: 2019/8/10 + */ +@Slf4j +public class ExecuteShellUtil { + + private Vector stdout; + + Session session; + + public ExecuteShellUtil(final String ipAddress, final String username, final String password,int port) { + try { + JSch jsch = new JSch(); + session = jsch.getSession(username, ipAddress, port); + session.setPassword(password); + session.setConfig("StrictHostKeyChecking", "no"); + session.connect(3000); + } catch (Exception e) { + log.error(e.getMessage(),e); + } + + } + + public int execute(final String command) { + int returnCode = 0; + ChannelShell channel = null; + PrintWriter printWriter = null; + BufferedReader input = null; + stdout = new Vector(); + try { + channel = (ChannelShell) session.openChannel("shell"); + channel.connect(); + input = new BufferedReader(new InputStreamReader(channel.getInputStream())); + printWriter = new PrintWriter(channel.getOutputStream()); + printWriter.println(command); + printWriter.println("exit"); + printWriter.flush(); + log.info("The remote command is: "); + String line; + while ((line = input.readLine()) != null) { + stdout.add(line); + System.out.println(line); + } + } catch (Exception e) { + log.error(e.getMessage(),e); + return -1; + }finally { + IoUtil.close(printWriter); + IoUtil.close(input); + if (channel != null) { + channel.disconnect(); + } + } + return returnCode; + } + + public void close(){ + if (session != null) { + session.disconnect(); + } + } + + public String executeForResult(String command) { + execute(command); + StringBuilder sb = new StringBuilder(); + for (String str : stdout) { + sb.append(str); + } + return sb.toString(); + } + +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/util/ScpClientUtil.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/util/ScpClientUtil.java new file mode 100644 index 0000000..7cb83aa --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/util/ScpClientUtil.java @@ -0,0 +1,105 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.util; + +import ch.ethz.ssh2.Connection; +import ch.ethz.ssh2.SCPClient; +import com.google.common.collect.Maps; + +import java.io.IOException; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * 远程执行linux命令 + * @author: ZhangHouYing + * @date: 2019-08-10 10:06 + */ +public class ScpClientUtil { + + static private Map instance = Maps.newHashMap(); + + static synchronized public ScpClientUtil getInstance(String ip, int port, String username, String password) { + if (instance.get(ip) == null) { + instance.put(ip, new ScpClientUtil(ip, port, username, password)); + } + return instance.get(ip); + } + + public ScpClientUtil(String ip, int port, String username, String password) { + this.ip = ip; + this.port = port; + this.username = username; + this.password = password; + } + + public void getFile(String remoteFile, String localTargetDirectory) { + Connection conn = new Connection(ip, port); + try { + conn.connect(); + boolean isAuthenticated = conn.authenticateWithPassword(username, password); + if (!isAuthenticated) { + System.err.println("authentication failed"); + } + SCPClient client = new SCPClient(conn); + client.get(remoteFile, localTargetDirectory); + } catch (IOException ex) { + Logger.getLogger(SCPClient.class.getName()).log(Level.SEVERE, null, ex); + }finally{ + conn.close(); + } + } + + public void putFile(String localFile, String remoteTargetDirectory) { + putFile(localFile, null, remoteTargetDirectory); + } + + public void putFile(String localFile, String remoteFileName, String remoteTargetDirectory) { + putFile(localFile, remoteFileName, remoteTargetDirectory,null); + } + + public void putFile(String localFile, String remoteFileName, String remoteTargetDirectory, String mode) { + Connection conn = new Connection(ip, port); + try { + conn.connect(); + boolean isAuthenticated = conn.authenticateWithPassword(username, password); + if (!isAuthenticated) { + System.err.println("authentication failed"); + } + SCPClient client = new SCPClient(conn); + if ((mode == null) || (mode.length() == 0)) { + mode = "0600"; + } + if (remoteFileName == null) { + client.put(localFile, remoteTargetDirectory); + } else { + client.put(localFile, remoteFileName, remoteTargetDirectory, mode); + } + } catch (IOException ex) { + Logger.getLogger(ScpClientUtil.class.getName()).log(Level.SEVERE, null, ex); + }finally{ + conn.close(); + } + } + + private String ip; + private int port; + private String username; + private String password; + + +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/util/SqlUtils.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/util/SqlUtils.java new file mode 100644 index 0000000..d7e06b1 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/util/SqlUtils.java @@ -0,0 +1,201 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.util; + +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.util.StringUtils; +import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; +import me.zhengjie.utils.CloseUtil; +import javax.sql.DataSource; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.sql.*; +import java.util.List; + +/** + * @author / + */ +@Slf4j +public class SqlUtils { + + /** + * 获取数据源 + * + * @param jdbcUrl / + * @param userName / + * @param password / + * @return DataSource + */ + private static DataSource getDataSource(String jdbcUrl, String userName, String password) { + DruidDataSource druidDataSource = new DruidDataSource(); + String className; + try { + className = DriverManager.getDriver(jdbcUrl.trim()).getClass().getName(); + } catch (SQLException e) { + throw new RuntimeException("Get class name error: =" + jdbcUrl); + } + if (StringUtils.isEmpty(className)) { + DataTypeEnum dataTypeEnum = DataTypeEnum.urlOf(jdbcUrl); + if (null == dataTypeEnum) { + throw new RuntimeException("Not supported data type: jdbcUrl=" + jdbcUrl); + } + druidDataSource.setDriverClassName(dataTypeEnum.getDriver()); + } else { + druidDataSource.setDriverClassName(className); + } + + + druidDataSource.setUrl(jdbcUrl); + druidDataSource.setUsername(userName); + druidDataSource.setPassword(password); + // 配置获取连接等待超时的时间 + druidDataSource.setMaxWait(3000); + // 配置初始化大小、最小、最大 + druidDataSource.setInitialSize(1); + druidDataSource.setMinIdle(1); + druidDataSource.setMaxActive(1); + + // 如果链接出现异常则直接判定为失败而不是一直重试 + druidDataSource.setBreakAfterAcquireFailure(true); + try { + druidDataSource.init(); + } catch (SQLException e) { + log.error("Exception during pool initialization", e); + throw new RuntimeException(e.getMessage()); + } + + return druidDataSource; + } + + private static Connection getConnection(String jdbcUrl, String userName, String password) { + DataSource dataSource = getDataSource(jdbcUrl, userName, password); + Connection connection = null; + try { + connection = dataSource.getConnection(); + } catch (Exception ignored) {} + try { + int timeOut = 5; + if (null == connection || connection.isClosed() || !connection.isValid(timeOut)) { + log.info("connection is closed or invalid, retry get connection!"); + connection = dataSource.getConnection(); + } + } catch (Exception e) { + log.error("create connection error, jdbcUrl: {}", jdbcUrl); + throw new RuntimeException("create connection error, jdbcUrl: " + jdbcUrl); + } finally { + CloseUtil.close(connection); + } + return connection; + } + + private static void releaseConnection(Connection connection) { + if (null != connection) { + try { + connection.close(); + } catch (Exception e) { + log.error(e.getMessage(),e); + log.error("connection close error:" + e.getMessage()); + } + } + } + + public static boolean testConnection(String jdbcUrl, String userName, String password) { + Connection connection = null; + try { + connection = getConnection(jdbcUrl, userName, password); + if (null != connection) { + return true; + } + } catch (Exception e) { + log.info("Get connection failed:" + e.getMessage()); + } finally { + releaseConnection(connection); + } + return false; + } + + public static String executeFile(String jdbcUrl, String userName, String password, File sqlFile) { + Connection connection = getConnection(jdbcUrl, userName, password); + try { + batchExecute(connection, readSqlList(sqlFile)); + } catch (Exception e) { + log.error("sql脚本执行发生异常:{}",e.getMessage()); + return e.getMessage(); + }finally { + releaseConnection(connection); + } + return "success"; + } + + + /** + * 批量执行sql + * @param connection / + * @param sqlList / + */ + public static void batchExecute(Connection connection, List sqlList) { + Statement st = null; + try { + st = connection.createStatement(); + for (String sql : sqlList) { + if (sql.endsWith(";")) { + sql = sql.substring(0, sql.length() - 1); + } + st.addBatch(sql); + } + st.executeBatch(); + } catch (SQLException throwables) { + throwables.printStackTrace(); + } finally { + CloseUtil.close(st); + } + } + + /** + * 将文件中的sql语句以;为单位读取到列表中 + * @param sqlFile / + * @return / + * @throws Exception e + */ + private static List readSqlList(File sqlFile) throws Exception { + List sqlList = Lists.newArrayList(); + StringBuilder sb = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader( + new FileInputStream(sqlFile), StandardCharsets.UTF_8))) { + String tmp; + while ((tmp = reader.readLine()) != null) { + log.info("line:{}", tmp); + if (tmp.endsWith(";")) { + sb.append(tmp); + sqlList.add(sb.toString()); + sb.delete(0, sb.length()); + } else { + sb.append(tmp); + } + } + if (!"".endsWith(sb.toString().trim())) { + sqlList.add(sb.toString()); + } + } + + return sqlList; + } + +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/websocket/MsgType.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/websocket/MsgType.java new file mode 100644 index 0000000..2fc473d --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/websocket/MsgType.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.websocket; + +/** + * @author ZhangHouYing + * @date 2019-08-10 9:56 + */ +public enum MsgType { + /** 连接 */ + CONNECT, + /** 关闭 */ + CLOSE, + /** 信息 */ + INFO, + /** 错误 */ + ERROR +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/websocket/SocketMsg.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/websocket/SocketMsg.java new file mode 100644 index 0000000..ade33a2 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/websocket/SocketMsg.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.websocket; + +import lombok.Data; + +/** + * @author ZhangHouYing + * @date 2019-08-10 9:55 + */ +@Data +public class SocketMsg { + private String msg; + private MsgType msgType; + + public SocketMsg(String msg, MsgType msgType) { + this.msg = msg; + this.msgType = msgType; + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/mnt/websocket/WebSocketServer.java b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/websocket/WebSocketServer.java new file mode 100644 index 0000000..000a515 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/mnt/websocket/WebSocketServer.java @@ -0,0 +1,134 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.mnt.websocket; + +import com.alibaba.fastjson.JSON; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import javax.websocket.*; +import javax.websocket.server.PathParam; +import javax.websocket.server.ServerEndpoint; +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArraySet; +/** + * @author ZhangHouYing + * @date 2019-08-10 15:46 + */ +@ServerEndpoint("/webSocket/{sid}") +@Slf4j +@Component +public class WebSocketServer { + + /** + * concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。 + */ + private static final CopyOnWriteArraySet webSocketSet = new CopyOnWriteArraySet(); + + /** + * 与某个客户端的连接会话,需要通过它来给客户端发送数据 + */ + private Session session; + + /** + * 接收sid + */ + private String sid=""; + /** + * 连接建立成功调用的方法 + * */ + @OnOpen + public void onOpen(Session session,@PathParam("sid") String sid) { + this.session = session; + //如果存在就先删除一个,防止重复推送消息 + webSocketSet.removeIf(webSocket -> webSocket.sid.equals(sid)); + webSocketSet.add(this); + this.sid=sid; + } + + /** + * 连接关闭调用的方法 + */ + @OnClose + public void onClose() { + webSocketSet.remove(this); + } + + /** + * 收到客户端消息后调用的方法 + * @param message 客户端发送过来的消息*/ + @OnMessage + public void onMessage(String message, Session session) { + log.info("收到来"+sid+"的信息:"+message); + //群发消息 + for (WebSocketServer item : webSocketSet) { + try { + item.sendMessage(message); + } catch (IOException e) { + log.error(e.getMessage(),e); + } + } + } + + @OnError + public void onError(Session session, Throwable error) { + log.error("发生错误"); + error.printStackTrace(); + } + /** + * 实现服务器主动推送 + */ + private void sendMessage(String message) throws IOException { + this.session.getBasicRemote().sendText(message); + } + + + /** + * 群发自定义消息 + * */ + public static void sendInfo(SocketMsg socketMsg,@PathParam("sid") String sid) throws IOException { + String message = JSON.toJSONString(socketMsg); + log.info("推送消息到"+sid+",推送内容:"+message); + for (WebSocketServer item : webSocketSet) { + try { + //这里可以设定只推送给这个sid的,为null则全部推送 + if(sid==null) { + item.sendMessage(message); + }else if(item.sid.equals(sid)){ + item.sendMessage(message); + } + } catch (IOException ignored) { } + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + WebSocketServer that = (WebSocketServer) o; + return Objects.equals(session, that.session) && + Objects.equals(sid, that.sid); + } + + @Override + public int hashCode() { + return Objects.hash(session, sid); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/quartz/config/JobRunner.java b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/config/JobRunner.java new file mode 100644 index 0000000..a5a9c91 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/config/JobRunner.java @@ -0,0 +1,51 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.quartz.config; + +import lombok.RequiredArgsConstructor; +import me.zhengjie.modules.quartz.domain.QuartzJob; +import me.zhengjie.modules.quartz.repository.QuartzJobRepository; +import me.zhengjie.modules.quartz.utils.QuartzManage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; +import java.util.List; + +/** + * @author Zheng Jie + * @date 2019-01-07 + */ +@Component +@RequiredArgsConstructor +public class JobRunner implements ApplicationRunner { + private static final Logger log = LoggerFactory.getLogger(JobRunner.class); + private final QuartzJobRepository quartzJobRepository; + private final QuartzManage quartzManage; + + /** + * 项目启动时重新激活启用的定时任务 + * + * @param applicationArguments / + */ + @Override + public void run(ApplicationArguments applicationArguments) { + List quartzJobs = quartzJobRepository.findByIsPauseIsFalse(); + quartzJobs.forEach(quartzManage::addJob); + log.info("Timing task injection complete"); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/quartz/config/QuartzConfig.java b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/config/QuartzConfig.java new file mode 100644 index 0000000..99f2e50 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/config/QuartzConfig.java @@ -0,0 +1,52 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.quartz.config; + +import org.quartz.spi.TriggerFiredBundle; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.quartz.AdaptableJobFactory; +import org.springframework.stereotype.Component; + +/** + * 定时任务配置 + * @author / + * @date 2019-01-07 + */ +@Configuration +public class QuartzConfig { + + /** + * 解决Job中注入Spring Bean为null的问题 + */ + @Component("quartzJobFactory") + public static class QuartzJobFactory extends AdaptableJobFactory { + + private final AutowireCapableBeanFactory capableBeanFactory; + + public QuartzJobFactory(AutowireCapableBeanFactory capableBeanFactory) { + this.capableBeanFactory = capableBeanFactory; + } + + @Override + protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { + //调用父类的方法,把Job注入到spring中 + Object jobInstance = super.createJobInstance(bundle); + capableBeanFactory.autowireBean(jobInstance); + return jobInstance; + } + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/quartz/domain/QuartzJob.java b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/domain/QuartzJob.java new file mode 100644 index 0000000..39e11f4 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/domain/QuartzJob.java @@ -0,0 +1,85 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.quartz.domain; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; +import me.zhengjie.base.BaseEntity; +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * @author Zheng Jie + * @date 2019-01-07 + */ +@Getter +@Setter +@Entity +@Table(name = "sys_quartz_job") +public class QuartzJob extends BaseEntity implements Serializable { + + public static final String JOB_KEY = "JOB_KEY"; + + @Id + @Column(name = "job_id") + @NotNull(groups = {Update.class}) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Transient + @ApiModelProperty(value = "用于子任务唯一标识", hidden = true) + private String uuid; + + @ApiModelProperty(value = "定时器名称") + private String jobName; + + @NotBlank + @ApiModelProperty(value = "Bean名称") + private String beanName; + + @NotBlank + @ApiModelProperty(value = "方法名称") + private String methodName; + + @ApiModelProperty(value = "参数") + private String params; + + @NotBlank + @ApiModelProperty(value = "cron表达式") + private String cronExpression; + + @ApiModelProperty(value = "状态,暂时或启动") + private Boolean isPause = false; + + @ApiModelProperty(value = "负责人") + private String personInCharge; + + @ApiModelProperty(value = "报警邮箱") + private String email; + + @ApiModelProperty(value = "子任务") + private String subTask; + + @ApiModelProperty(value = "失败后暂停") + private Boolean pauseAfterFailure; + + @NotBlank + @ApiModelProperty(value = "备注") + private String description; +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/quartz/domain/QuartzLog.java b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/domain/QuartzLog.java new file mode 100644 index 0000000..4de57ec --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/domain/QuartzLog.java @@ -0,0 +1,67 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.quartz.domain; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.hibernate.annotations.CreationTimestamp; +import javax.persistence.*; +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * @author Zheng Jie + * @date 2019-01-07 + */ +@Entity +@Data +@Table(name = "sys_quartz_log") +public class QuartzLog implements Serializable { + + @Id + @Column(name = "log_id") + @ApiModelProperty(value = "ID", hidden = true) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ApiModelProperty(value = "任务名称", hidden = true) + private String jobName; + + @ApiModelProperty(value = "bean名称", hidden = true) + private String beanName; + + @ApiModelProperty(value = "方法名称", hidden = true) + private String methodName; + + @ApiModelProperty(value = "参数", hidden = true) + private String params; + + @ApiModelProperty(value = "cron表达式", hidden = true) + private String cronExpression; + + @ApiModelProperty(value = "状态", hidden = true) + private Boolean isSuccess; + + @ApiModelProperty(value = "异常详情", hidden = true) + private String exceptionDetail; + + @ApiModelProperty(value = "执行耗时", hidden = true) + private Long time; + + @CreationTimestamp + @ApiModelProperty(value = "创建时间", hidden = true) + private Timestamp createTime; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/quartz/repository/QuartzJobRepository.java b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/repository/QuartzJobRepository.java new file mode 100644 index 0000000..79b222b --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/repository/QuartzJobRepository.java @@ -0,0 +1,34 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.quartz.repository; + +import me.zhengjie.modules.quartz.domain.QuartzJob; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import java.util.List; + +/** + * @author Zheng Jie + * @date 2019-01-07 + */ +public interface QuartzJobRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * 查询启用的任务 + * @return List + */ + List findByIsPauseIsFalse(); +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/quartz/repository/QuartzLogRepository.java b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/repository/QuartzLogRepository.java new file mode 100644 index 0000000..db724aa --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/repository/QuartzLogRepository.java @@ -0,0 +1,28 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.quartz.repository; + +import me.zhengjie.modules.quartz.domain.QuartzLog; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** + * @author Zheng Jie + * @date 2019-01-07 + */ +public interface QuartzLogRepository extends JpaRepository, JpaSpecificationExecutor { + +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/quartz/rest/QuartzJobController.java b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/rest/QuartzJobController.java new file mode 100644 index 0000000..e1b5da7 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/rest/QuartzJobController.java @@ -0,0 +1,141 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.quartz.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.zhengjie.annotation.Log; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.modules.quartz.domain.QuartzJob; +import me.zhengjie.modules.quartz.domain.QuartzLog; +import me.zhengjie.modules.quartz.service.QuartzJobService; +import me.zhengjie.modules.quartz.service.dto.JobQueryCriteria; +import me.zhengjie.utils.PageResult; +import me.zhengjie.utils.SpringContextHolder; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Set; + +/** + * @author Zheng Jie + * @date 2019-01-07 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/jobs") +@Api(tags = "系统:定时任务管理") +public class QuartzJobController { + + private static final String ENTITY_NAME = "quartzJob"; + private final QuartzJobService quartzJobService; + + @ApiOperation("查询定时任务") + @GetMapping + @PreAuthorize("@el.check('timing:list')") + public ResponseEntity> queryQuartzJob(JobQueryCriteria criteria, Pageable pageable){ + return new ResponseEntity<>(quartzJobService.queryAll(criteria,pageable), HttpStatus.OK); + } + + @ApiOperation("导出任务数据") + @GetMapping(value = "/download") + @PreAuthorize("@el.check('timing:list')") + public void exportQuartzJob(HttpServletResponse response, JobQueryCriteria criteria) throws IOException { + quartzJobService.download(quartzJobService.queryAll(criteria), response); + } + + @ApiOperation("导出日志数据") + @GetMapping(value = "/logs/download") + @PreAuthorize("@el.check('timing:list')") + public void exportQuartzJobLog(HttpServletResponse response, JobQueryCriteria criteria) throws IOException { + quartzJobService.downloadLog(quartzJobService.queryAllLog(criteria), response); + } + + @ApiOperation("查询任务执行日志") + @GetMapping(value = "/logs") + @PreAuthorize("@el.check('timing:list')") + public ResponseEntity> queryQuartzJobLog(JobQueryCriteria criteria, Pageable pageable){ + return new ResponseEntity<>(quartzJobService.queryAllLog(criteria,pageable), HttpStatus.OK); + } + + @Log("新增定时任务") + @ApiOperation("新增定时任务") + @PostMapping + @PreAuthorize("@el.check('timing:add')") + public ResponseEntity createQuartzJob(@Validated @RequestBody QuartzJob resources){ + if (resources.getId() != null) { + throw new BadRequestException("A new "+ ENTITY_NAME +" cannot already have an ID"); + } + // 验证Bean是不是合法的,合法的定时任务 Bean 需要用 @Service 定义 + checkBean(resources.getBeanName()); + quartzJobService.create(resources); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @Log("修改定时任务") + @ApiOperation("修改定时任务") + @PutMapping + @PreAuthorize("@el.check('timing:edit')") + public ResponseEntity updateQuartzJob(@Validated(QuartzJob.Update.class) @RequestBody QuartzJob resources){ + // 验证Bean是不是合法的,合法的定时任务 Bean 需要用 @Service 定义 + checkBean(resources.getBeanName()); + quartzJobService.update(resources); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @Log("更改定时任务状态") + @ApiOperation("更改定时任务状态") + @PutMapping(value = "/{id}") + @PreAuthorize("@el.check('timing:edit')") + public ResponseEntity updateQuartzJobStatus(@PathVariable Long id){ + quartzJobService.updateIsPause(quartzJobService.findById(id)); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @Log("执行定时任务") + @ApiOperation("执行定时任务") + @PutMapping(value = "/exec/{id}") + @PreAuthorize("@el.check('timing:edit')") + public ResponseEntity executionQuartzJob(@PathVariable Long id){ + quartzJobService.execution(quartzJobService.findById(id)); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @Log("删除定时任务") + @ApiOperation("删除定时任务") + @DeleteMapping + @PreAuthorize("@el.check('timing:del')") + public ResponseEntity deleteQuartzJob(@RequestBody Set ids){ + quartzJobService.delete(ids); + return new ResponseEntity<>(HttpStatus.OK); + } + + private void checkBean(String beanName){ + // 避免调用攻击者可以从SpringContextHolder获得控制jdbcTemplate类 + // 并使用getDeclaredMethod调用jdbcTemplate的queryForMap函数,执行任意sql命令。 + if(!SpringContextHolder.getAllServiceBeanName().contains(beanName)){ + throw new BadRequestException("非法的 Bean,请重新输入!"); + } + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/quartz/service/QuartzJobService.java b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/service/QuartzJobService.java new file mode 100644 index 0000000..2e93c7b --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/service/QuartzJobService.java @@ -0,0 +1,123 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.quartz.service; + +import me.zhengjie.modules.quartz.domain.QuartzJob; +import me.zhengjie.modules.quartz.domain.QuartzLog; +import me.zhengjie.modules.quartz.service.dto.JobQueryCriteria; +import me.zhengjie.utils.PageResult; +import org.springframework.data.domain.Pageable; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +/** + * @author Zheng Jie + * @date 2019-01-07 + */ +public interface QuartzJobService { + + /** + * 分页查询 + * @param criteria 条件 + * @param pageable 分页参数 + * @return / + */ + PageResult queryAll(JobQueryCriteria criteria, Pageable pageable); + + /** + * 查询全部 + * @param criteria 条件 + * @return / + */ + List queryAll(JobQueryCriteria criteria); + + /** + * 分页查询日志 + * @param criteria 条件 + * @param pageable 分页参数 + * @return / + */ + PageResult queryAllLog(JobQueryCriteria criteria, Pageable pageable); + + /** + * 查询全部 + * @param criteria 条件 + * @return / + */ + List queryAllLog(JobQueryCriteria criteria); + + /** + * 创建 + * @param resources / + */ + void create(QuartzJob resources); + + /** + * 编辑 + * @param resources / + */ + void update(QuartzJob resources); + + /** + * 删除任务 + * @param ids / + */ + void delete(Set ids); + + /** + * 根据ID查询 + * @param id ID + * @return / + */ + QuartzJob findById(Long id); + + /** + * 更改定时任务状态 + * @param quartzJob / + */ + void updateIsPause(QuartzJob quartzJob); + + /** + * 立即执行定时任务 + * @param quartzJob / + */ + void execution(QuartzJob quartzJob); + + /** + * 导出定时任务 + * @param queryAll 待导出的数据 + * @param response / + * @throws IOException / + */ + void download(List queryAll, HttpServletResponse response) throws IOException; + + /** + * 导出定时任务日志 + * @param queryAllLog 待导出的数据 + * @param response / + * @throws IOException / + */ + void downloadLog(List queryAllLog, HttpServletResponse response) throws IOException; + + /** + * 执行子任务 + * @param tasks / + * @throws InterruptedException / + */ + void executionSubJob(String[] tasks) throws InterruptedException; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/quartz/service/dto/JobQueryCriteria.java b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/service/dto/JobQueryCriteria.java new file mode 100644 index 0000000..6cec0f1 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/service/dto/JobQueryCriteria.java @@ -0,0 +1,38 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.quartz.service.dto; + +import lombok.Data; +import me.zhengjie.annotation.Query; +import java.sql.Timestamp; +import java.util.List; + +/** + * @author Zheng Jie + * @date 2019-6-4 10:33:02 + */ +@Data +public class JobQueryCriteria { + + @Query(type = Query.Type.INNER_LIKE) + private String jobName; + + @Query + private Boolean isSuccess; + + @Query(type = Query.Type.BETWEEN) + private List createTime; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/quartz/service/impl/QuartzJobServiceImpl.java b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/service/impl/QuartzJobServiceImpl.java new file mode 100644 index 0000000..06341e2 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/service/impl/QuartzJobServiceImpl.java @@ -0,0 +1,197 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.quartz.service.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.modules.quartz.domain.QuartzJob; +import me.zhengjie.modules.quartz.domain.QuartzLog; +import me.zhengjie.modules.quartz.repository.QuartzJobRepository; +import me.zhengjie.modules.quartz.repository.QuartzLogRepository; +import me.zhengjie.modules.quartz.service.QuartzJobService; +import me.zhengjie.modules.quartz.service.dto.JobQueryCriteria; +import me.zhengjie.modules.quartz.utils.QuartzManage; +import me.zhengjie.utils.*; +import org.quartz.CronExpression; +import org.springframework.data.domain.Pageable; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; + +/** + * @author Zheng Jie + * @date 2019-01-07 + */ +@RequiredArgsConstructor +@Service(value = "quartzJobService") +public class QuartzJobServiceImpl implements QuartzJobService { + + private final QuartzJobRepository quartzJobRepository; + private final QuartzLogRepository quartzLogRepository; + private final QuartzManage quartzManage; + private final RedisUtils redisUtils; + + @Override + public PageResult queryAll(JobQueryCriteria criteria, Pageable pageable){ + return PageUtil.toPage(quartzJobRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder),pageable)); + } + + @Override + public PageResult queryAllLog(JobQueryCriteria criteria, Pageable pageable){ + return PageUtil.toPage(quartzLogRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder),pageable)); + } + + @Override + public List queryAll(JobQueryCriteria criteria) { + return quartzJobRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder)); + } + + @Override + public List queryAllLog(JobQueryCriteria criteria) { + return quartzLogRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder)); + } + + @Override + public QuartzJob findById(Long id) { + QuartzJob quartzJob = quartzJobRepository.findById(id).orElseGet(QuartzJob::new); + ValidationUtil.isNull(quartzJob.getId(),"QuartzJob","id",id); + return quartzJob; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void create(QuartzJob resources) { + if (!CronExpression.isValidExpression(resources.getCronExpression())){ + throw new BadRequestException("cron表达式格式错误"); + } + resources = quartzJobRepository.save(resources); + quartzManage.addJob(resources); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(QuartzJob resources) { + if (!CronExpression.isValidExpression(resources.getCronExpression())){ + throw new BadRequestException("cron表达式格式错误"); + } + if(StringUtils.isNotBlank(resources.getSubTask())){ + List tasks = Arrays.asList(resources.getSubTask().split("[,,]")); + if (tasks.contains(resources.getId().toString())) { + throw new BadRequestException("子任务中不能添加当前任务ID"); + } + } + resources = quartzJobRepository.save(resources); + quartzManage.updateJobCron(resources); + } + + @Override + public void updateIsPause(QuartzJob quartzJob) { + if (quartzJob.getIsPause()) { + quartzManage.resumeJob(quartzJob); + quartzJob.setIsPause(false); + } else { + quartzManage.pauseJob(quartzJob); + quartzJob.setIsPause(true); + } + quartzJobRepository.save(quartzJob); + } + + @Override + public void execution(QuartzJob quartzJob) { + quartzManage.runJobNow(quartzJob); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Set ids) { + for (Long id : ids) { + QuartzJob quartzJob = findById(id); + quartzManage.deleteJob(quartzJob); + quartzJobRepository.delete(quartzJob); + } + } + + @Async + @Override + @Transactional(rollbackFor = Exception.class) + public void executionSubJob(String[] tasks) throws InterruptedException { + for (String id : tasks) { + if (StrUtil.isBlank(id)) { + // 如果是手动清除子任务id,会出现id为空字符串的问题 + continue; + } + QuartzJob quartzJob = findById(Long.parseLong(id)); + // 执行任务 + String uuid = IdUtil.simpleUUID(); + quartzJob.setUuid(uuid); + // 执行任务 + execution(quartzJob); + // 获取执行状态,如果执行失败则停止后面的子任务执行 + Boolean result = (Boolean) redisUtils.get(uuid); + while (result == null) { + // 休眠5秒,再次获取子任务执行情况 + Thread.sleep(5000); + result = (Boolean) redisUtils.get(uuid); + } + if(!result){ + redisUtils.del(uuid); + break; + } + } + } + + @Override + public void download(List quartzJobs, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (QuartzJob quartzJob : quartzJobs) { + Map map = new LinkedHashMap<>(); + map.put("任务名称", quartzJob.getJobName()); + map.put("Bean名称", quartzJob.getBeanName()); + map.put("执行方法", quartzJob.getMethodName()); + map.put("参数", quartzJob.getParams()); + map.put("表达式", quartzJob.getCronExpression()); + map.put("状态", quartzJob.getIsPause() ? "暂停中" : "运行中"); + map.put("描述", quartzJob.getDescription()); + map.put("创建日期", quartzJob.getCreateTime()); + list.add(map); + } + FileUtil.downloadExcel(list, response); + } + + @Override + public void downloadLog(List queryAllLog, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (QuartzLog quartzLog : queryAllLog) { + Map map = new LinkedHashMap<>(); + map.put("任务名称", quartzLog.getJobName()); + map.put("Bean名称", quartzLog.getBeanName()); + map.put("执行方法", quartzLog.getMethodName()); + map.put("参数", quartzLog.getParams()); + map.put("表达式", quartzLog.getCronExpression()); + map.put("异常详情", quartzLog.getExceptionDetail()); + map.put("耗时/毫秒", quartzLog.getTime()); + map.put("状态", quartzLog.getIsSuccess() ? "成功" : "失败"); + map.put("创建日期", quartzLog.getCreateTime()); + list.add(map); + } + FileUtil.downloadExcel(list, response); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/quartz/task/TestTask.java b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/task/TestTask.java new file mode 100644 index 0000000..4cf3434 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/task/TestTask.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.quartz.task; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 测试用 + * @author Zheng Jie + * @date 2019-01-08 + */ +@Slf4j +@Service +public class TestTask { + + public void run(){ + log.info("run 执行成功"); + } + + public void run1(String str){ + log.info("run1 执行成功,参数为: {}" + str); + } + + public void run2(){ + log.info("run2 执行成功"); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/quartz/utils/ExecutionJob.java b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/utils/ExecutionJob.java new file mode 100644 index 0000000..4de1120 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/utils/ExecutionJob.java @@ -0,0 +1,133 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.quartz.utils; + +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import me.zhengjie.domain.vo.EmailVo; +import me.zhengjie.modules.quartz.domain.QuartzJob; +import me.zhengjie.modules.quartz.domain.QuartzLog; +import me.zhengjie.modules.quartz.repository.QuartzLogRepository; +import me.zhengjie.modules.quartz.service.QuartzJobService; +import me.zhengjie.service.EmailService; +import me.zhengjie.utils.RedisUtils; +import me.zhengjie.utils.SpringContextHolder; +import me.zhengjie.utils.StringUtils; +import me.zhengjie.utils.ThrowableUtil; +import org.quartz.JobExecutionContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.scheduling.quartz.QuartzJobBean; +import java.util.*; +import java.util.concurrent.*; + +/** + * 参考人人开源,... + * @author / + * @date 2019-01-07 + */ +@Async +public class ExecutionJob extends QuartzJobBean { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + // 此处仅供参考,可根据任务执行情况自定义线程池参数 + private final ThreadPoolTaskExecutor executor = SpringContextHolder.getBean("elAsync"); + + @Override + public void executeInternal(JobExecutionContext context) { + // 获取任务 + QuartzJob quartzJob = (QuartzJob) context.getMergedJobDataMap().get(QuartzJob.JOB_KEY); + // 获取spring bean + QuartzLogRepository quartzLogRepository = SpringContextHolder.getBean(QuartzLogRepository.class); + QuartzJobService quartzJobService = SpringContextHolder.getBean(QuartzJobService.class); + RedisUtils redisUtils = SpringContextHolder.getBean(RedisUtils.class); + + String uuid = quartzJob.getUuid(); + + QuartzLog log = new QuartzLog(); + log.setJobName(quartzJob.getJobName()); + log.setBeanName(quartzJob.getBeanName()); + log.setMethodName(quartzJob.getMethodName()); + log.setParams(quartzJob.getParams()); + long startTime = System.currentTimeMillis(); + log.setCronExpression(quartzJob.getCronExpression()); + try { + // 执行任务 + QuartzRunnable task = new QuartzRunnable(quartzJob.getBeanName(), quartzJob.getMethodName(), quartzJob.getParams()); + Future future = executor.submit(task); + future.get(); + long times = System.currentTimeMillis() - startTime; + log.setTime(times); + if(StringUtils.isNotBlank(uuid)) { + redisUtils.set(uuid, true); + } + // 任务状态 + log.setIsSuccess(true); + logger.info("任务执行成功,任务名称:" + quartzJob.getJobName() + ", 执行时间:" + times + "毫秒"); + // 判断是否存在子任务 + if(StringUtils.isNotBlank(quartzJob.getSubTask())){ + String[] tasks = quartzJob.getSubTask().split("[,,]"); + // 执行子任务 + quartzJobService.executionSubJob(tasks); + } + } catch (Exception e) { + if(StringUtils.isNotBlank(uuid)) { + redisUtils.set(uuid, false); + } + logger.error("任务执行失败,任务名称:" + quartzJob.getJobName()); + long times = System.currentTimeMillis() - startTime; + log.setTime(times); + // 任务状态 0:成功 1:失败 + log.setIsSuccess(false); + log.setExceptionDetail(ThrowableUtil.getStackTrace(e)); + // 任务如果失败了则暂停 + if(quartzJob.getPauseAfterFailure() != null && quartzJob.getPauseAfterFailure()){ + quartzJob.setIsPause(false); + //更新状态 + quartzJobService.updateIsPause(quartzJob); + } + if(quartzJob.getEmail() != null){ + EmailService emailService = SpringContextHolder.getBean(EmailService.class); + // 邮箱报警 + if(StringUtils.isNoneBlank(quartzJob.getEmail())){ + EmailVo emailVo = taskAlarm(quartzJob, ThrowableUtil.getStackTrace(e)); + emailService.send(emailVo, emailService.find()); + } + } + } finally { + quartzLogRepository.save(log); + } + } + + private EmailVo taskAlarm(QuartzJob quartzJob, String msg) { + EmailVo emailVo = new EmailVo(); + emailVo.setSubject("定时任务【"+ quartzJob.getJobName() +"】执行失败,请尽快处理!"); + Map data = new HashMap<>(16); + data.put("task", quartzJob); + data.put("msg", msg); + TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("template", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = engine.getTemplate("taskAlarm.ftl"); + emailVo.setContent(template.render(data)); + List emails = Arrays.asList(quartzJob.getEmail().split("[,,]")); + emailVo.setTos(emails); + return emailVo; + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/quartz/utils/QuartzManage.java b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/utils/QuartzManage.java new file mode 100644 index 0000000..e0cf1e0 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/utils/QuartzManage.java @@ -0,0 +1,174 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.quartz.utils; + +import lombok.extern.slf4j.Slf4j; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.modules.quartz.domain.QuartzJob; +import org.quartz.*; +import org.quartz.impl.triggers.CronTriggerImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import javax.annotation.Resource; +import java.util.Date; +import static org.quartz.TriggerBuilder.newTrigger; + +/** + * @author Zheng Jie + * @date 2019-01-07 + */ +@Slf4j +@Component +public class QuartzManage { + + private static final String JOB_NAME = "TASK_"; + + @Resource + private Scheduler scheduler; + + public void addJob(QuartzJob quartzJob){ + try { + // 构建job信息 + JobDetail jobDetail = JobBuilder.newJob(ExecutionJob.class). + withIdentity(JOB_NAME + quartzJob.getId()).build(); + + //通过触发器名和cron 表达式创建 Trigger + Trigger cronTrigger = newTrigger() + .withIdentity(JOB_NAME + quartzJob.getId()) + .startNow() + .withSchedule(CronScheduleBuilder.cronSchedule(quartzJob.getCronExpression())) + .build(); + + cronTrigger.getJobDataMap().put(QuartzJob.JOB_KEY, quartzJob); + + //重置启动时间 + ((CronTriggerImpl)cronTrigger).setStartTime(new Date()); + + //执行定时任务 + scheduler.scheduleJob(jobDetail,cronTrigger); + + // 暂停任务 + if (quartzJob.getIsPause()) { + pauseJob(quartzJob); + } + } catch (Exception e){ + log.error("创建定时任务失败", e); + throw new BadRequestException("创建定时任务失败"); + } + } + + /** + * 更新job cron表达式 + * @param quartzJob / + */ + public void updateJobCron(QuartzJob quartzJob){ + try { + TriggerKey triggerKey = TriggerKey.triggerKey(JOB_NAME + quartzJob.getId()); + CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey); + // 如果不存在则创建一个定时任务 + if(trigger == null){ + addJob(quartzJob); + trigger = (CronTrigger) scheduler.getTrigger(triggerKey); + } + CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(quartzJob.getCronExpression()); + trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build(); + //重置启动时间 + ((CronTriggerImpl)trigger).setStartTime(new Date()); + trigger.getJobDataMap().put(QuartzJob.JOB_KEY,quartzJob); + + scheduler.rescheduleJob(triggerKey, trigger); + // 暂停任务 + if (quartzJob.getIsPause()) { + pauseJob(quartzJob); + } + } catch (Exception e){ + log.error("更新定时任务失败", e); + throw new BadRequestException("更新定时任务失败"); + } + + } + + /** + * 删除一个job + * @param quartzJob / + */ + public void deleteJob(QuartzJob quartzJob){ + try { + JobKey jobKey = JobKey.jobKey(JOB_NAME + quartzJob.getId()); + scheduler.pauseJob(jobKey); + scheduler.deleteJob(jobKey); + } catch (Exception e){ + log.error("删除定时任务失败", e); + throw new BadRequestException("删除定时任务失败"); + } + } + + /** + * 恢复一个job + * @param quartzJob / + */ + public void resumeJob(QuartzJob quartzJob){ + try { + TriggerKey triggerKey = TriggerKey.triggerKey(JOB_NAME + quartzJob.getId()); + CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey); + // 如果不存在则创建一个定时任务 + if(trigger == null) { + addJob(quartzJob); + } + JobKey jobKey = JobKey.jobKey(JOB_NAME + quartzJob.getId()); + scheduler.resumeJob(jobKey); + } catch (Exception e){ + log.error("恢复定时任务失败", e); + throw new BadRequestException("恢复定时任务失败"); + } + } + + /** + * 立即执行job + * @param quartzJob / + */ + public void runJobNow(QuartzJob quartzJob){ + try { + TriggerKey triggerKey = TriggerKey.triggerKey(JOB_NAME + quartzJob.getId()); + CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey); + // 如果不存在则创建一个定时任务 + if(trigger == null) { + addJob(quartzJob); + } + JobDataMap dataMap = new JobDataMap(); + dataMap.put(QuartzJob.JOB_KEY, quartzJob); + JobKey jobKey = JobKey.jobKey(JOB_NAME + quartzJob.getId()); + scheduler.triggerJob(jobKey,dataMap); + } catch (Exception e){ + log.error("定时任务执行失败", e); + throw new BadRequestException("定时任务执行失败"); + } + } + + /** + * 暂停一个job + * @param quartzJob / + */ + public void pauseJob(QuartzJob quartzJob){ + try { + JobKey jobKey = JobKey.jobKey(JOB_NAME + quartzJob.getId()); + scheduler.pauseJob(jobKey); + } catch (Exception e){ + log.error("定时任务暂停失败", e); + throw new BadRequestException("定时任务暂停失败"); + } + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/quartz/utils/QuartzRunnable.java b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/utils/QuartzRunnable.java new file mode 100644 index 0000000..31c3a0f --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/quartz/utils/QuartzRunnable.java @@ -0,0 +1,58 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.quartz.utils; + +import lombok.extern.slf4j.Slf4j; +import me.zhengjie.utils.SpringContextHolder; +import org.apache.commons.lang3.StringUtils; +import org.springframework.util.ReflectionUtils; +import java.lang.reflect.Method; +import java.util.concurrent.Callable; + +/** + * 执行定时任务 + * @author / + */ +@Slf4j +public class QuartzRunnable implements Callable { + + private final Object target; + private final Method method; + private final String params; + + QuartzRunnable(String beanName, String methodName, String params) + throws NoSuchMethodException, SecurityException { + this.target = SpringContextHolder.getBean(beanName); + this.params = params; + if (StringUtils.isNotBlank(params)) { + this.method = target.getClass().getDeclaredMethod(methodName, String.class); + } else { + this.method = target.getClass().getDeclaredMethod(methodName); + } + } + + @Override + @SuppressWarnings("all") + public Object call() throws Exception { + ReflectionUtils.makeAccessible(method); + if (StringUtils.isNotBlank(params)) { + method.invoke(target, params); + } else { + method.invoke(target); + } + return null; + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/security/config/ConfigBeanConfiguration.java b/eladmin-system/src/main/java/me/zhengjie/modules/security/config/ConfigBeanConfiguration.java new file mode 100644 index 0000000..8cbc88d --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/security/config/ConfigBeanConfiguration.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.security.config; + +import me.zhengjie.modules.security.config.bean.LoginProperties; +import me.zhengjie.modules.security.config.bean.SecurityProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @apiNote 配置文件转换Pojo类的 统一配置 类 + * @author: liaojinlong + * @date: 2020/6/10 19:04 + */ +@Configuration +public class ConfigBeanConfiguration { + + @Bean + @ConfigurationProperties(prefix = "login") + public LoginProperties loginProperties() { + return new LoginProperties(); + } + + @Bean + @ConfigurationProperties(prefix = "jwt") + public SecurityProperties securityProperties() { + return new SecurityProperties(); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/security/config/SpringSecurityConfig.java b/eladmin-system/src/main/java/me/zhengjie/modules/security/config/SpringSecurityConfig.java new file mode 100644 index 0000000..ad886f3 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/security/config/SpringSecurityConfig.java @@ -0,0 +1,188 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.security.config; + +import lombok.RequiredArgsConstructor; +import me.zhengjie.annotation.AnonymousAccess; +import me.zhengjie.modules.security.config.bean.SecurityProperties; +import me.zhengjie.modules.security.security.*; +import me.zhengjie.modules.security.service.OnlineUserService; +import me.zhengjie.modules.security.service.UserCacheManager; +import me.zhengjie.utils.enums.RequestMethodEnum; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.core.GrantedAuthorityDefaults; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.filter.CorsFilter; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import java.util.*; + +/** + * @author Zheng Jie + */ +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) +public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { + + private final TokenProvider tokenProvider; + private final CorsFilter corsFilter; + private final JwtAuthenticationEntryPoint authenticationErrorHandler; + private final JwtAccessDeniedHandler jwtAccessDeniedHandler; + private final ApplicationContext applicationContext; + private final SecurityProperties properties; + private final OnlineUserService onlineUserService; + private final UserCacheManager userCacheManager; + + @Bean + GrantedAuthorityDefaults grantedAuthorityDefaults() { + // 去除 ROLE_ 前缀 + return new GrantedAuthorityDefaults(""); + } + + @Bean + public PasswordEncoder passwordEncoder() { + // 密码加密方式 + return new BCryptPasswordEncoder(); + } + + @Override + protected void configure(HttpSecurity httpSecurity) throws Exception { + // 搜寻匿名标记 url: @AnonymousAccess + RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping"); + Map handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods(); + // 获取匿名标记 + Map> anonymousUrls = getAnonymousUrl(handlerMethodMap); + httpSecurity + // 禁用 CSRF + .csrf().disable() + .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) + // 授权异常 + .exceptionHandling() + .authenticationEntryPoint(authenticationErrorHandler) + .accessDeniedHandler(jwtAccessDeniedHandler) + // 防止iframe 造成跨域 + .and() + .headers() + .frameOptions() + .disable() + // 不创建会话 + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + // 静态资源等等 + .antMatchers( + HttpMethod.GET, + "/*.html", + "/**/*.html", + "/**/*.css", + "/**/*.js", + "/webSocket/**" + ).permitAll() + // swagger 文档 + .antMatchers("/swagger-ui.html").permitAll() + .antMatchers("/swagger-resources/**").permitAll() + .antMatchers("/webjars/**").permitAll() + .antMatchers("/*/api-docs").permitAll() + // 文件 + .antMatchers("/avatar/**").permitAll() + .antMatchers("/file/**").permitAll() + // 阿里巴巴 druid + .antMatchers("/druid/**").permitAll() + // 放行OPTIONS请求 + .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() + // 自定义匿名访问所有url放行:允许匿名和带Token访问,细腻化到每个 Request 类型 + // GET + .antMatchers(HttpMethod.GET, anonymousUrls.get(RequestMethodEnum.GET.getType()).toArray(new String[0])).permitAll() + // POST + .antMatchers(HttpMethod.POST, anonymousUrls.get(RequestMethodEnum.POST.getType()).toArray(new String[0])).permitAll() + // PUT + .antMatchers(HttpMethod.PUT, anonymousUrls.get(RequestMethodEnum.PUT.getType()).toArray(new String[0])).permitAll() + // PATCH + .antMatchers(HttpMethod.PATCH, anonymousUrls.get(RequestMethodEnum.PATCH.getType()).toArray(new String[0])).permitAll() + // DELETE + .antMatchers(HttpMethod.DELETE, anonymousUrls.get(RequestMethodEnum.DELETE.getType()).toArray(new String[0])).permitAll() + // 所有类型的接口都放行 + .antMatchers(anonymousUrls.get(RequestMethodEnum.ALL.getType()).toArray(new String[0])).permitAll() + // 所有请求都需要认证 + .anyRequest().authenticated() + .and().apply(securityConfigurerAdapter()); + } + + private TokenConfigurer securityConfigurerAdapter() { + return new TokenConfigurer(tokenProvider, properties, onlineUserService, userCacheManager); + } + + private Map> getAnonymousUrl(Map handlerMethodMap) { + Map> anonymousUrls = new HashMap<>(8); + Set get = new HashSet<>(); + Set post = new HashSet<>(); + Set put = new HashSet<>(); + Set patch = new HashSet<>(); + Set delete = new HashSet<>(); + Set all = new HashSet<>(); + for (Map.Entry infoEntry : handlerMethodMap.entrySet()) { + HandlerMethod handlerMethod = infoEntry.getValue(); + AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class); + if (null != anonymousAccess) { + List requestMethods = new ArrayList<>(infoEntry.getKey().getMethodsCondition().getMethods()); + RequestMethodEnum request = RequestMethodEnum.find(requestMethods.size() == 0 ? RequestMethodEnum.ALL.getType() : requestMethods.get(0).name()); + switch (Objects.requireNonNull(request)) { + case GET: + get.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); + break; + case POST: + post.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); + break; + case PUT: + put.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); + break; + case PATCH: + patch.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); + break; + case DELETE: + delete.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); + break; + default: + all.addAll(infoEntry.getKey().getPatternsCondition().getPatterns()); + break; + } + } + } + anonymousUrls.put(RequestMethodEnum.GET.getType(), get); + anonymousUrls.put(RequestMethodEnum.POST.getType(), post); + anonymousUrls.put(RequestMethodEnum.PUT.getType(), put); + anonymousUrls.put(RequestMethodEnum.PATCH.getType(), patch); + anonymousUrls.put(RequestMethodEnum.DELETE.getType(), delete); + anonymousUrls.put(RequestMethodEnum.ALL.getType(), all); + return anonymousUrls; + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/security/config/bean/LoginCode.java b/eladmin-system/src/main/java/me/zhengjie/modules/security/config/bean/LoginCode.java new file mode 100644 index 0000000..fefd252 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/security/config/bean/LoginCode.java @@ -0,0 +1,61 @@ +/* + * Copyright 2019-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.security.config.bean; + +import lombok.Data; + +/** + * 登录验证码配置信息 + * + * @author liaojinlong + * @date 2020/6/10 18:53 + */ +@Data +public class LoginCode { + + /** + * 验证码配置 + */ + private LoginCodeEnum codeType; + /** + * 验证码有效期 分钟 + */ + private Long expiration = 2L; + /** + * 验证码内容长度 + */ + private int length = 2; + /** + * 验证码宽度 + */ + private int width = 111; + /** + * 验证码高度 + */ + private int height = 36; + /** + * 验证码字体 + */ + private String fontName; + /** + * 字体大小 + */ + private int fontSize = 25; + + public LoginCodeEnum getCodeType() { + return codeType; + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/security/config/bean/LoginCodeEnum.java b/eladmin-system/src/main/java/me/zhengjie/modules/security/config/bean/LoginCodeEnum.java new file mode 100644 index 0000000..685ccbb --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/security/config/bean/LoginCodeEnum.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.security.config.bean; + +/** + * 验证码配置枚举 + * + * @author: liaojinlong + * @date: 2020/6/10 17:40 + */ + +public enum LoginCodeEnum { + /** + * 算数 + */ + ARITHMETIC, + /** + * 中文 + */ + CHINESE, + /** + * 中文闪图 + */ + CHINESE_GIF, + /** + * 闪图 + */ + GIF, + SPEC +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/security/config/bean/LoginProperties.java b/eladmin-system/src/main/java/me/zhengjie/modules/security/config/bean/LoginProperties.java new file mode 100644 index 0000000..0201a13 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/security/config/bean/LoginProperties.java @@ -0,0 +1,125 @@ +/* + * Copyright 2019-2020 the original author or authors. + * + * Licensed under the Apache License, Version loginCode.length.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-loginCode.length.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.security.config.bean; + +import com.wf.captcha.*; +import com.wf.captcha.base.Captcha; +import lombok.Data; +import me.zhengjie.exception.BadConfigurationException; +import me.zhengjie.utils.StringUtils; +import java.awt.*; +import java.util.Objects; + +/** + * 配置文件读取 + * + * @author liaojinlong + * @date loginCode.length0loginCode.length0/6/10 17:loginCode.length6 + */ +@Data +public class LoginProperties { + + /** + * 账号单用户 登录 + */ + private boolean singleLogin = false; + + private LoginCode loginCode; + + public static final String cacheKey = "user-login-cache:"; + + public boolean isSingleLogin() { + return singleLogin; + } + + /** + * 获取验证码生产类 + * + * @return / + */ + public Captcha getCaptcha() { + if (Objects.isNull(loginCode)) { + loginCode = new LoginCode(); + if (Objects.isNull(loginCode.getCodeType())) { + loginCode.setCodeType(LoginCodeEnum.ARITHMETIC); + } + } + return switchCaptcha(loginCode); + } + + /** + * 依据配置信息生产验证码 + * + * @param loginCode 验证码配置信息 + * @return / + */ + private Captcha switchCaptcha(LoginCode loginCode) { + Captcha captcha; + switch (loginCode.getCodeType()) { + case ARITHMETIC: + // 算术类型 https://gitee.com/whvse/EasyCaptcha + captcha = new FixedArithmeticCaptcha(loginCode.getWidth(), loginCode.getHeight()); + // 几位数运算,默认是两位 + captcha.setLen(loginCode.getLength()); + break; + case CHINESE: + captcha = new ChineseCaptcha(loginCode.getWidth(), loginCode.getHeight()); + captcha.setLen(loginCode.getLength()); + break; + case CHINESE_GIF: + captcha = new ChineseGifCaptcha(loginCode.getWidth(), loginCode.getHeight()); + captcha.setLen(loginCode.getLength()); + break; + case GIF: + captcha = new GifCaptcha(loginCode.getWidth(), loginCode.getHeight()); + captcha.setLen(loginCode.getLength()); + break; + case SPEC: + captcha = new SpecCaptcha(loginCode.getWidth(), loginCode.getHeight()); + captcha.setLen(loginCode.getLength()); + break; + default: + throw new BadConfigurationException("验证码配置信息错误!正确配置查看 LoginCodeEnum "); + } + if(StringUtils.isNotBlank(loginCode.getFontName())){ + captcha.setFont(new Font(loginCode.getFontName(), Font.PLAIN, loginCode.getFontSize())); + } + return captcha; + } + + static class FixedArithmeticCaptcha extends ArithmeticCaptcha { + public FixedArithmeticCaptcha(int width, int height) { + super(width, height); + } + + @Override + protected char[] alphas() { + // 生成随机数字和运算符 + int n1 = num(1, 10), n2 = num(1, 10); + int opt = num(3); + + // 计算结果 + int res = new int[]{n1 + n2, n1 - n2, n1 * n2}[opt]; + // 转换为字符运算符 + char optChar = "+-x".charAt(opt); + + this.setArithmeticString(String.format("%s%c%s=?", n1, optChar, n2)); + this.chars = String.valueOf(res); + + return chars.toCharArray(); + } + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/security/config/bean/SecurityProperties.java b/eladmin-system/src/main/java/me/zhengjie/modules/security/config/bean/SecurityProperties.java new file mode 100644 index 0000000..16ec3cf --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/security/config/bean/SecurityProperties.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.security.config.bean; + +import lombok.Data; + +/** + * Jwt参数配置 + * + * @author Zheng Jie + * @date 2019年11月28日 + */ +@Data +public class SecurityProperties { + + /** + * Request Headers : Authorization + */ + private String header; + + /** + * 令牌前缀,最后留个空格 Bearer + */ + private String tokenStartWith; + + /** + * 必须使用最少88位的Base64对该令牌进行编码 + */ + private String base64Secret; + + /** + * 令牌过期时间 此处单位/毫秒 + */ + private Long tokenValidityInSeconds; + + /** + * 在线用户 key,根据 key 查询 redis 中在线用户的数据 + */ + private String onlineKey; + + /** + * 验证码 key + */ + private String codeKey; + + /** + * token 续期检查 + */ + private Long detect; + + /** + * 续期时间 + */ + private Long renew; + + public String getTokenStartWith() { + return tokenStartWith + " "; + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/security/rest/AuthorizationController.java b/eladmin-system/src/main/java/me/zhengjie/modules/security/rest/AuthorizationController.java new file mode 100644 index 0000000..effb15f --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/security/rest/AuthorizationController.java @@ -0,0 +1,149 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.security.rest; + +import cn.hutool.core.util.IdUtil; +import com.wf.captcha.base.Captcha; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.zhengjie.annotation.Log; +import me.zhengjie.annotation.rest.AnonymousDeleteMapping; +import me.zhengjie.annotation.rest.AnonymousGetMapping; +import me.zhengjie.annotation.rest.AnonymousPostMapping; +import me.zhengjie.config.RsaProperties; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.modules.security.config.bean.LoginCodeEnum; +import me.zhengjie.modules.security.config.bean.LoginProperties; +import me.zhengjie.modules.security.config.bean.SecurityProperties; +import me.zhengjie.modules.security.security.TokenProvider; +import me.zhengjie.modules.security.service.dto.AuthUserDto; +import me.zhengjie.modules.security.service.dto.JwtUserDto; +import me.zhengjie.modules.security.service.OnlineUserService; +import me.zhengjie.utils.RsaUtils; +import me.zhengjie.utils.RedisUtils; +import me.zhengjie.utils.SecurityUtils; +import me.zhengjie.utils.StringUtils; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * @author Zheng Jie + * @date 2018-11-23 + * 授权、根据token获取用户详细信息 + */ +@Slf4j +@RestController +@RequestMapping("/auth") +@RequiredArgsConstructor +@Api(tags = "系统:系统授权接口") +public class AuthorizationController { + private final SecurityProperties properties; + private final RedisUtils redisUtils; + private final OnlineUserService onlineUserService; + private final TokenProvider tokenProvider; + private final AuthenticationManagerBuilder authenticationManagerBuilder; + @Resource + private LoginProperties loginProperties; + + @Log("用户登录") + @ApiOperation("登录授权") + @AnonymousPostMapping(value = "/login") + public ResponseEntity login(@Validated @RequestBody AuthUserDto authUser, HttpServletRequest request) throws Exception { + // 密码解密 + String password = RsaUtils.decryptByPrivateKey(RsaProperties.privateKey, authUser.getPassword()); + // 查询验证码 + String code = (String) redisUtils.get(authUser.getUuid()); + // 清除验证码 + redisUtils.del(authUser.getUuid()); + if (StringUtils.isBlank(code)) { + throw new BadRequestException("验证码不存在或已过期"); + } + if (StringUtils.isBlank(authUser.getCode()) || !authUser.getCode().equalsIgnoreCase(code)) { + throw new BadRequestException("验证码错误"); + } + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(authUser.getUsername(), password); + Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken); + SecurityContextHolder.getContext().setAuthentication(authentication); + // 生成令牌与第三方系统获取令牌方式 + // UserDetails userDetails = userDetailsService.loadUserByUsername(userInfo.getUsername()); + // Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + // SecurityContextHolder.getContext().setAuthentication(authentication); + String token = tokenProvider.createToken(authentication); + final JwtUserDto jwtUserDto = (JwtUserDto) authentication.getPrincipal(); + // 返回 token 与 用户信息 + Map authInfo = new HashMap(2) {{ + put("token", properties.getTokenStartWith() + token); + put("user", jwtUserDto); + }}; + if (loginProperties.isSingleLogin()) { + // 踢掉之前已经登录的token + onlineUserService.kickOutForUsername(authUser.getUsername()); + } + // 保存在线信息 + onlineUserService.save(jwtUserDto, token, request); + // 返回登录信息 + return ResponseEntity.ok(authInfo); + } + + @ApiOperation("获取用户信息") + @GetMapping(value = "/info") + public ResponseEntity getUserInfo() { + return ResponseEntity.ok(SecurityUtils.getCurrentUser()); + } + + @ApiOperation("获取验证码") + @AnonymousGetMapping(value = "/code") + public ResponseEntity getCode() { + // 获取运算的结果 + Captcha captcha = loginProperties.getCaptcha(); + String uuid = properties.getCodeKey() + IdUtil.simpleUUID(); + //当验证码类型为 arithmetic时且长度 >= 2 时,captcha.text()的结果有几率为浮点型 + String captchaValue = captcha.text(); + if (captcha.getCharType() - 1 == LoginCodeEnum.ARITHMETIC.ordinal() && captchaValue.contains(".")) { + captchaValue = captchaValue.split("\\.")[0]; + } + // 保存 + redisUtils.set(uuid, captchaValue, loginProperties.getLoginCode().getExpiration(), TimeUnit.MINUTES); + // 验证码信息 + Map imgResult = new HashMap(2) {{ + put("img", captcha.toBase64()); + put("uuid", uuid); + }}; + return ResponseEntity.ok(imgResult); + } + + @ApiOperation("退出登录") + @AnonymousDeleteMapping(value = "/logout") + public ResponseEntity logout(HttpServletRequest request) { + onlineUserService.logout(tokenProvider.getToken(request)); + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/security/rest/OnlineController.java b/eladmin-system/src/main/java/me/zhengjie/modules/security/rest/OnlineController.java new file mode 100644 index 0000000..2e46859 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/security/rest/OnlineController.java @@ -0,0 +1,70 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.security.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import me.zhengjie.modules.security.service.OnlineUserService; +import me.zhengjie.modules.security.service.dto.OnlineUserDto; +import me.zhengjie.utils.EncryptUtils; +import me.zhengjie.utils.PageResult; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Set; + +/** + * @author Zheng Jie + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/auth/online") +@Api(tags = "系统:在线用户管理") +public class OnlineController { + + private final OnlineUserService onlineUserService; + + @ApiOperation("查询在线用户") + @GetMapping + @PreAuthorize("@el.check()") + public ResponseEntity> queryOnlineUser(String username, Pageable pageable){ + return new ResponseEntity<>(onlineUserService.getAll(username, pageable),HttpStatus.OK); + } + + @ApiOperation("导出数据") + @GetMapping(value = "/download") + @PreAuthorize("@el.check()") + public void exportOnlineUser(HttpServletResponse response, String username) throws IOException { + onlineUserService.download(onlineUserService.getAll(username), response); + } + + @ApiOperation("踢出用户") + @DeleteMapping + @PreAuthorize("@el.check()") + public ResponseEntity deleteOnlineUser(@RequestBody Set keys) throws Exception { + for (String token : keys) { + // 解密Key + token = EncryptUtils.desDecrypt(token); + onlineUserService.logout(token); + } + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/security/security/JwtAccessDeniedHandler.java b/eladmin-system/src/main/java/me/zhengjie/modules/security/security/JwtAccessDeniedHandler.java new file mode 100644 index 0000000..8b3f8c1 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/security/security/JwtAccessDeniedHandler.java @@ -0,0 +1,37 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.security.security; + +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author Zheng Jie + */ +@Component +public class JwtAccessDeniedHandler implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { + //当用户在没有授权的情况下访问受保护的REST资源时,将调用此方法发送403 Forbidden响应 + response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage()); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/security/security/JwtAuthenticationEntryPoint.java b/eladmin-system/src/main/java/me/zhengjie/modules/security/security/JwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..f881586 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/security/security/JwtAuthenticationEntryPoint.java @@ -0,0 +1,39 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.security.security; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author Zheng Jie + */ +@Component +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws IOException { + // 当用户尝试访问安全的REST资源而不提供任何凭据时,将调用此方法发送401 响应 + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException==null?"Unauthorized":authException.getMessage()); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/security/security/TokenConfigurer.java b/eladmin-system/src/main/java/me/zhengjie/modules/security/security/TokenConfigurer.java new file mode 100644 index 0000000..cff5e1e --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/security/security/TokenConfigurer.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.security.security; + +import lombok.RequiredArgsConstructor; +import me.zhengjie.modules.security.config.bean.SecurityProperties; +import me.zhengjie.modules.security.service.OnlineUserService; +import me.zhengjie.modules.security.service.UserCacheManager; +import org.springframework.security.config.annotation.SecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +/** + * @author / + */ +@RequiredArgsConstructor +public class TokenConfigurer extends SecurityConfigurerAdapter { + + private final TokenProvider tokenProvider; + private final SecurityProperties properties; + private final OnlineUserService onlineUserService; + private final UserCacheManager userCacheManager; + + @Override + public void configure(HttpSecurity http) { + TokenFilter customFilter = new TokenFilter(tokenProvider, properties, onlineUserService, userCacheManager); + http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/security/security/TokenFilter.java b/eladmin-system/src/main/java/me/zhengjie/modules/security/security/TokenFilter.java new file mode 100644 index 0000000..7cb8a03 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/security/security/TokenFilter.java @@ -0,0 +1,109 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.security.security; + +import cn.hutool.core.util.StrUtil; +import io.jsonwebtoken.ExpiredJwtException; +import me.zhengjie.modules.security.config.bean.SecurityProperties; +import me.zhengjie.modules.security.service.UserCacheManager; +import me.zhengjie.modules.security.service.dto.OnlineUserDto; +import me.zhengjie.modules.security.service.OnlineUserService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.GenericFilterBean; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.Objects; + +/** + * @author / + */ +public class TokenFilter extends GenericFilterBean { + private static final Logger log = LoggerFactory.getLogger(TokenFilter.class); + + + private final TokenProvider tokenProvider; + private final SecurityProperties properties; + private final OnlineUserService onlineUserService; + private final UserCacheManager userCacheManager; + + /** + * @param tokenProvider Token + * @param properties JWT + * @param onlineUserService 用户在线 + * @param userCacheManager 用户缓存工具 + */ + public TokenFilter(TokenProvider tokenProvider, SecurityProperties properties, OnlineUserService onlineUserService, UserCacheManager userCacheManager) { + this.properties = properties; + this.onlineUserService = onlineUserService; + this.tokenProvider = tokenProvider; + this.userCacheManager = userCacheManager; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + String token = resolveToken(httpServletRequest); + // 对于 Token 为空的不需要去查 Redis + if (StrUtil.isNotBlank(token)) { + OnlineUserDto onlineUserDto = null; + boolean cleanUserCache = false; + try { + String loginKey = tokenProvider.loginKey(token); + onlineUserDto = onlineUserService.getOne(loginKey); + } catch (ExpiredJwtException e) { + log.error(e.getMessage()); + cleanUserCache = true; + } finally { + if (cleanUserCache || Objects.isNull(onlineUserDto)) { + userCacheManager.cleanUserCache(String.valueOf(tokenProvider.getClaims(token).get(TokenProvider.AUTHORITIES_KEY))); + } + } + if (onlineUserDto != null && StringUtils.hasText(token)) { + Authentication authentication = tokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + // Token 续期 + tokenProvider.checkRenewal(token); + } + } + filterChain.doFilter(servletRequest, servletResponse); + } + + /** + * 初步检测Token + * + * @param request / + * @return / + */ + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader(properties.getHeader()); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(properties.getTokenStartWith())) { + // 去掉令牌前缀 + return bearerToken.replace(properties.getTokenStartWith(), ""); + } else { + log.debug("非法Token:{}", bearerToken); + } + return null; + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/security/security/TokenProvider.java b/eladmin-system/src/main/java/me/zhengjie/modules/security/security/TokenProvider.java new file mode 100644 index 0000000..d2db83c --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/security/security/TokenProvider.java @@ -0,0 +1,135 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.security.security; + +import cn.hutool.core.date.DateField; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.crypto.digest.DigestUtil; +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; +import me.zhengjie.modules.security.config.bean.SecurityProperties; +import me.zhengjie.utils.RedisUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.User; +import org.springframework.stereotype.Component; +import javax.servlet.http.HttpServletRequest; +import java.security.Key; +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * @author / + */ +@Slf4j +@Component +public class TokenProvider implements InitializingBean { + + private final SecurityProperties properties; + private final RedisUtils redisUtils; + public static final String AUTHORITIES_KEY = "user"; + private JwtParser jwtParser; + private JwtBuilder jwtBuilder; + + public TokenProvider(SecurityProperties properties, RedisUtils redisUtils) { + this.properties = properties; + this.redisUtils = redisUtils; + } + + @Override + public void afterPropertiesSet() { + byte[] keyBytes = Decoders.BASE64.decode(properties.getBase64Secret()); + Key key = Keys.hmacShaKeyFor(keyBytes); + jwtParser = Jwts.parserBuilder() + .setSigningKey(key) + .build(); + jwtBuilder = Jwts.builder() + .signWith(key, SignatureAlgorithm.HS512); + } + + /** + * 创建Token 设置永不过期, + * Token 的时间有效性转到Redis 维护 + * + * @param authentication / + * @return / + */ + public String createToken(Authentication authentication) { + return jwtBuilder + // 加入ID确保生成的 Token 都不一致 + .setId(IdUtil.simpleUUID()) + .claim(AUTHORITIES_KEY, authentication.getName()) + .setSubject(authentication.getName()) + .compact(); + } + + /** + * 依据Token 获取鉴权信息 + * + * @param token / + * @return / + */ + Authentication getAuthentication(String token) { + Claims claims = getClaims(token); + User principal = new User(claims.getSubject(), "******", new ArrayList<>()); + return new UsernamePasswordAuthenticationToken(principal, token, new ArrayList<>()); + } + + public Claims getClaims(String token) { + return jwtParser + .parseClaimsJws(token) + .getBody(); + } + + /** + * @param token 需要检查的token + */ + public void checkRenewal(String token) { + // 判断是否续期token,计算token的过期时间 + long time = redisUtils.getExpire(properties.getOnlineKey() + token) * 1000; + Date expireDate = DateUtil.offset(new Date(), DateField.MILLISECOND, (int) time); + // 判断当前时间与过期时间的时间差 + long differ = expireDate.getTime() - System.currentTimeMillis(); + // 如果在续期检查的范围内,则续期 + if (differ <= properties.getDetect()) { + long renew = time + properties.getRenew(); + redisUtils.expire(properties.getOnlineKey() + token, renew, TimeUnit.MILLISECONDS); + } + } + + public String getToken(HttpServletRequest request) { + final String requestHeader = request.getHeader(properties.getHeader()); + if (requestHeader != null && requestHeader.startsWith(properties.getTokenStartWith())) { + return requestHeader.substring(7); + } + return null; + } + + /** + * 获取登录用户RedisKey + * @param token / + * @return key + */ + public String loginKey(String token) { + Claims claims = getClaims(token); + String md5Token = DigestUtil.md5Hex(token); + return properties.getOnlineKey() + claims.getSubject() + "-" + md5Token; + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/security/service/OnlineUserService.java b/eladmin-system/src/main/java/me/zhengjie/modules/security/service/OnlineUserService.java new file mode 100644 index 0000000..ede7868 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/security/service/OnlineUserService.java @@ -0,0 +1,149 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.security.service; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.zhengjie.modules.security.security.TokenProvider; +import me.zhengjie.utils.PageResult; +import me.zhengjie.modules.security.config.bean.SecurityProperties; +import me.zhengjie.modules.security.service.dto.JwtUserDto; +import me.zhengjie.modules.security.service.dto.OnlineUserDto; +import me.zhengjie.utils.*; +import org.springframework.data.domain.Pageable; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * @author Zheng Jie + * @date 2019年10月26日21:56:27 + */ +@Service +@Slf4j +@AllArgsConstructor +public class OnlineUserService { + + private final SecurityProperties properties; + private final TokenProvider tokenProvider; + private final RedisUtils redisUtils; + + /** + * 保存在线用户信息 + * @param jwtUserDto / + * @param token / + * @param request / + */ + public void save(JwtUserDto jwtUserDto, String token, HttpServletRequest request){ + String dept = jwtUserDto.getUser().getDept().getName(); + String ip = StringUtils.getIp(request); + String browser = StringUtils.getBrowser(request); + String address = StringUtils.getCityInfo(ip); + OnlineUserDto onlineUserDto = null; + try { + onlineUserDto = new OnlineUserDto(jwtUserDto.getUsername(), jwtUserDto.getUser().getNickName(), dept, browser , ip, address, EncryptUtils.desEncrypt(token), new Date()); + } catch (Exception e) { + log.error(e.getMessage(),e); + } + String loginKey = tokenProvider.loginKey(token); + redisUtils.set(loginKey, onlineUserDto, properties.getTokenValidityInSeconds(), TimeUnit.MILLISECONDS); + } + + /** + * 查询全部数据 + * @param username / + * @param pageable / + * @return / + */ + public PageResult getAll(String username, Pageable pageable){ + List onlineUserDtos = getAll(username); + return PageUtil.toPage( + PageUtil.paging(pageable.getPageNumber(),pageable.getPageSize(), onlineUserDtos), + onlineUserDtos.size() + ); + } + + /** + * 查询全部数据,不分页 + * @param username / + * @return / + */ + public List getAll(String username){ + String loginKey = properties.getOnlineKey() + + (StringUtils.isBlank(username) ? "" : "*" + username); + List keys = redisUtils.scan(loginKey + "*"); + Collections.reverse(keys); + List onlineUserDtos = new ArrayList<>(); + for (String key : keys) { + onlineUserDtos.add((OnlineUserDto) redisUtils.get(key)); + } + onlineUserDtos.sort((o1, o2) -> o2.getLoginTime().compareTo(o1.getLoginTime())); + return onlineUserDtos; + } + + /** + * 退出登录 + * @param token / + */ + public void logout(String token) { + String loginKey = tokenProvider.loginKey(token); + redisUtils.del(loginKey); + } + + /** + * 导出 + * @param all / + * @param response / + * @throws IOException / + */ + public void download(List all, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (OnlineUserDto user : all) { + Map map = new LinkedHashMap<>(); + map.put("用户名", user.getUserName()); + map.put("部门", user.getDept()); + map.put("登录IP", user.getIp()); + map.put("登录地点", user.getAddress()); + map.put("浏览器", user.getBrowser()); + map.put("登录日期", user.getLoginTime()); + list.add(map); + } + FileUtil.downloadExcel(list, response); + } + + /** + * 查询用户 + * @param key / + * @return / + */ + public OnlineUserDto getOne(String key) { + return (OnlineUserDto)redisUtils.get(key); + } + + /** + * 根据用户名强退用户 + * @param username / + */ + @Async + public void kickOutForUsername(String username) { + String loginKey = properties.getOnlineKey() + username + "*"; + redisUtils.scanDel(loginKey); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/security/service/UserCacheManager.java b/eladmin-system/src/main/java/me/zhengjie/modules/security/service/UserCacheManager.java new file mode 100644 index 0000000..0808e65 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/security/service/UserCacheManager.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.security.service; + +import cn.hutool.core.util.RandomUtil; +import me.zhengjie.modules.security.config.bean.LoginProperties; +import me.zhengjie.modules.security.service.dto.JwtUserDto; +import me.zhengjie.utils.RedisUtils; +import me.zhengjie.utils.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import javax.annotation.Resource; + +/** + * @author Zheng Jie + * @description 用户缓存管理 + * @date 2022-05-26 + **/ +@Component +public class UserCacheManager { + + @Resource + private RedisUtils redisUtils; + @Value("${login.user-cache.idle-time}") + private long idleTime; + + /** + * 返回用户缓存 + * @param userName 用户名 + * @return JwtUserDto + */ + public JwtUserDto getUserCache(String userName) { + if (StringUtils.isNotEmpty(userName)) { + // 获取数据 + Object obj = redisUtils.get(LoginProperties.cacheKey + userName); + if(obj != null){ + return (JwtUserDto)obj; + } + } + return null; + } + + /** + * 添加缓存到Redis + * @param userName 用户名 + */ + @Async + public void addUserCache(String userName, JwtUserDto user) { + if (StringUtils.isNotEmpty(userName)) { + // 添加数据, 避免数据同时过期 + long time = idleTime + RandomUtil.randomInt(900, 1800); + redisUtils.set(LoginProperties.cacheKey + userName, user, time); + } + } + + /** + * 清理用户缓存信息 + * 用户信息变更时 + * @param userName 用户名 + */ + @Async + public void cleanUserCache(String userName) { + if (StringUtils.isNotEmpty(userName)) { + // 清除数据 + redisUtils.del(LoginProperties.cacheKey + userName); + } + } +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/security/service/UserDetailsServiceImpl.java b/eladmin-system/src/main/java/me/zhengjie/modules/security/service/UserDetailsServiceImpl.java new file mode 100644 index 0000000..70db5df --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/security/service/UserDetailsServiceImpl.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.security.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.exception.EntityNotFoundException; +import me.zhengjie.modules.security.service.dto.JwtUserDto; +import me.zhengjie.modules.system.service.DataService; +import me.zhengjie.modules.system.service.RoleService; +import me.zhengjie.modules.system.service.UserService; +import me.zhengjie.modules.system.service.dto.UserLoginDto; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +/** + * @author Zheng Jie + * @date 2018-11-22 + */ +@Slf4j +@RequiredArgsConstructor +@Service("userDetailsService") +public class UserDetailsServiceImpl implements UserDetailsService { + private final UserService userService; + private final RoleService roleService; + private final DataService dataService; + private final UserCacheManager userCacheManager; + + @Override + public JwtUserDto loadUserByUsername(String username) { + JwtUserDto jwtUserDto = userCacheManager.getUserCache(username); + if(jwtUserDto == null){ + UserLoginDto user; + try { + user = userService.getLoginData(username); + } catch (EntityNotFoundException e) { + // SpringSecurity会自动转换UsernameNotFoundException为BadCredentialsException + throw new UsernameNotFoundException(username, e); + } + if (user == null) { + throw new UsernameNotFoundException(""); + } else { + if (!user.getEnabled()) { + throw new BadRequestException("账号未激活!"); + } + jwtUserDto = new JwtUserDto( + user, + dataService.getDeptIds(user), + roleService.mapToGrantedAuthorities(user) + ); + // 添加缓存数据 + userCacheManager.addUserCache(username, jwtUserDto); + } + } + return jwtUserDto; + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/security/service/dto/AuthUserDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/security/service/dto/AuthUserDto.java new file mode 100644 index 0000000..5219fc5 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/security/service/dto/AuthUserDto.java @@ -0,0 +1,39 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.security.service.dto; + +import lombok.Getter; +import lombok.Setter; +import javax.validation.constraints.NotBlank; + +/** + * @author Zheng Jie + * @date 2018-11-30 + */ +@Getter +@Setter +public class AuthUserDto { + + @NotBlank + private String username; + + @NotBlank + private String password; + + private String code; + + private String uuid = ""; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/security/service/dto/AuthorityDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/security/service/dto/AuthorityDto.java new file mode 100644 index 0000000..888cceb --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/security/service/dto/AuthorityDto.java @@ -0,0 +1,34 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.security.service.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; + +/** + * 避免序列化问题 + * @author Zheng Jie + * @date 2018-11-30 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AuthorityDto implements GrantedAuthority { + + private String authority; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/security/service/dto/JwtUserDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/security/service/dto/JwtUserDto.java new file mode 100644 index 0000000..5aa0c45 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/security/service/dto/JwtUserDto.java @@ -0,0 +1,80 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.security.service.dto; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.AllArgsConstructor; +import lombok.Getter; +import me.zhengjie.modules.system.service.dto.UserLoginDto; +import org.springframework.security.core.userdetails.UserDetails; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author Zheng Jie + * @date 2018-11-23 + */ +@Getter +@AllArgsConstructor +public class JwtUserDto implements UserDetails { + + private final UserLoginDto user; + + private final List dataScopes; + + private final List authorities; + + public Set getRoles() { + return authorities.stream().map(AuthorityDto::getAuthority).collect(Collectors.toSet()); + } + + @Override + @JSONField(serialize = false) + public String getPassword() { + return user.getPassword(); + } + + @Override + @JSONField(serialize = false) + public String getUsername() { + return user.getUsername(); + } + + @JSONField(serialize = false) + @Override + public boolean isAccountNonExpired() { + return true; + } + + @JSONField(serialize = false) + @Override + public boolean isAccountNonLocked() { + return true; + } + + @JSONField(serialize = false) + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + @JSONField(serialize = false) + public boolean isEnabled() { + return user.getEnabled(); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/security/service/dto/OnlineUserDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/security/service/dto/OnlineUserDto.java new file mode 100644 index 0000000..290ab6d --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/security/service/dto/OnlineUserDto.java @@ -0,0 +1,73 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.security.service.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.util.Date; + +/** + * 在线用户 + * @author Zheng Jie + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class OnlineUserDto { + + /** + * 用户名 + */ + private String userName; + + /** + * 昵称 + */ + private String nickName; + + /** + * 岗位 + */ + private String dept; + + /** + * 浏览器 + */ + private String browser; + + /** + * IP + */ + private String ip; + + /** + * 地址 + */ + private String address; + + /** + * token + */ + private String key; + + /** + * 登录时间 + */ + private Date loginTime; + + +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Dept.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Dept.java new file mode 100644 index 0000000..9f28af2 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Dept.java @@ -0,0 +1,86 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.domain; + +import com.alibaba.fastjson.annotation.JSONField; +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; +import me.zhengjie.base.BaseEntity; +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.Objects; +import java.util.Set; + +/** +* @author Zheng Jie +* @date 2019-03-25 +*/ +@Entity +@Getter +@Setter +@Table(name="sys_dept") +public class Dept extends BaseEntity implements Serializable { + + @Id + @Column(name = "dept_id") + @NotNull(groups = Update.class) + @ApiModelProperty(value = "ID", hidden = true) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JSONField(serialize = false) + @ManyToMany(mappedBy = "depts") + @ApiModelProperty(value = "角色") + private Set roles; + + @ApiModelProperty(value = "排序") + private Integer deptSort; + + @NotBlank + @ApiModelProperty(value = "部门名称") + private String name; + + @NotNull + @ApiModelProperty(value = "是否启用") + private Boolean enabled; + + @ApiModelProperty(value = "上级部门") + private Long pid; + + @ApiModelProperty(value = "子节点数目", hidden = true) + private Integer subCount = 0; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Dept dept = (Dept) o; + return Objects.equals(id, dept.id) && + Objects.equals(name, dept.name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name); + } +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Dict.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Dict.java new file mode 100644 index 0000000..689cf2e --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Dict.java @@ -0,0 +1,54 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.domain; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; +import me.zhengjie.base.BaseEntity; +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.List; + +/** +* @author Zheng Jie +* @date 2019-04-10 +*/ +@Entity +@Getter +@Setter +@Table(name="sys_dict") +public class Dict extends BaseEntity implements Serializable { + + @Id + @Column(name = "dict_id") + @NotNull(groups = Update.class) + @ApiModelProperty(value = "ID", hidden = true) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToMany(mappedBy = "dict",cascade={CascadeType.PERSIST,CascadeType.REMOVE}) + private List dictDetails; + + @NotBlank + @ApiModelProperty(value = "名称") + private String name; + + @ApiModelProperty(value = "描述") + private String description; +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/DictDetail.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/DictDetail.java new file mode 100644 index 0000000..554dde6 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/DictDetail.java @@ -0,0 +1,56 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.domain; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; +import me.zhengjie.base.BaseEntity; +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** +* @author Zheng Jie +* @date 2019-04-10 +*/ +@Entity +@Getter +@Setter +@Table(name="sys_dict_detail") +public class DictDetail extends BaseEntity implements Serializable { + + @Id + @Column(name = "detail_id") + @NotNull(groups = Update.class) + @ApiModelProperty(value = "ID", hidden = true) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(name = "dict_id") + @ManyToOne(fetch=FetchType.LAZY) + @ApiModelProperty(value = "字典", hidden = true) + private Dict dict; + + @ApiModelProperty(value = "字典标签") + private String label; + + @ApiModelProperty(value = "字典值") + private String value; + + @ApiModelProperty(value = "排序") + private Integer dictSort = 999; +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Job.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Job.java new file mode 100644 index 0000000..f2d358a --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Job.java @@ -0,0 +1,73 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.domain; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; +import me.zhengjie.base.BaseEntity; +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.Objects; + +/** +* @author Zheng Jie +* @date 2019-03-29 +*/ +@Entity +@Getter +@Setter +@Table(name="sys_job") +public class Job extends BaseEntity implements Serializable { + + @Id + @Column(name = "job_id") + @NotNull(groups = Update.class) + @ApiModelProperty(value = "ID", hidden = true) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank + @ApiModelProperty(value = "岗位名称") + private String name; + + @NotNull + @ApiModelProperty(value = "岗位排序") + private Long jobSort; + + @NotNull + @ApiModelProperty(value = "是否启用") + private Boolean enabled; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Job job = (Job) o; + return Objects.equals(id, job.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Menu.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Menu.java new file mode 100644 index 0000000..b2ea225 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Menu.java @@ -0,0 +1,110 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.domain; + +import com.alibaba.fastjson.annotation.JSONField; +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; +import me.zhengjie.base.BaseEntity; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.Objects; +import java.util.Set; + +/** + * @author Zheng Jie + * @date 2018-12-17 + */ +@Entity +@Getter +@Setter +@Table(name = "sys_menu") +public class Menu extends BaseEntity implements Serializable { + + @Id + @Column(name = "menu_id") + @NotNull(groups = {Update.class}) + @ApiModelProperty(value = "ID", hidden = true) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JSONField(serialize = false) + @ManyToMany(mappedBy = "menus") + @ApiModelProperty(value = "菜单角色") + private Set roles; + + @ApiModelProperty(value = "菜单标题") + private String title; + + @Column(name = "name") + @ApiModelProperty(value = "菜单组件名称") + private String componentName; + + @ApiModelProperty(value = "排序") + private Integer menuSort = 999; + + @ApiModelProperty(value = "组件路径") + private String component; + + @ApiModelProperty(value = "路由地址") + private String path; + + @ApiModelProperty(value = "菜单类型,目录、菜单、按钮") + private Integer type; + + @ApiModelProperty(value = "权限标识") + private String permission; + + @ApiModelProperty(value = "菜单图标") + private String icon; + + @Column(columnDefinition = "bit(1) default 0") + @ApiModelProperty(value = "缓存") + private Boolean cache; + + @Column(columnDefinition = "bit(1) default 0") + @ApiModelProperty(value = "是否隐藏") + private Boolean hidden; + + @ApiModelProperty(value = "上级菜单") + private Long pid; + + @ApiModelProperty(value = "子节点数目", hidden = true) + private Integer subCount = 0; + + @ApiModelProperty(value = "外链菜单") + private Boolean iFrame; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Menu menu = (Menu) o; + return Objects.equals(id, menu.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Purchase.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Purchase.java new file mode 100644 index 0000000..61c6255 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Purchase.java @@ -0,0 +1,115 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.domain; + +import lombok.Data; +import cn.hutool.core.bean.BeanUtil; +import io.swagger.annotations.ApiModelProperty; +import cn.hutool.core.bean.copier.CopyOptions; +import javax.persistence.*; +import javax.validation.constraints.*; +import javax.persistence.Entity; +import javax.persistence.Table; + +import me.zhengjie.base.BaseEntity; +import org.hibernate.annotations.*; +import java.sql.Timestamp; +import java.math.BigDecimal; +import java.io.Serializable; + +/** +* @website https://eladmin.vip +* @description / +* @author author +* +**/ +@Entity +@Data +@Table(name="purchase") +public class Purchase extends BaseEntity implements Serializable { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "`purchase_id`") + @ApiModelProperty(value = "ID") + private Long purchaseId; + + @ApiModelProperty(value = "申请人") + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + @Column(name = "`reason`") + @ApiModelProperty(value = "申请事由") + private String reason; + + @Column(name = "`fee`") + @ApiModelProperty(value = "申请金额") + private String fee; + + @Column(name = "`product_name`") + @ApiModelProperty(value = "货物/服务名称") + private String productName; + + @Column(name = "`purchase_type`") + @ApiModelProperty(value = "申请类型") + private String purchaseType; + + @Column(name = "`product_info`") + @ApiModelProperty(value = "品牌/规格/参数") + private String productInfo; + + @Column(name = "`product_count`") + @ApiModelProperty(value = "数量") + private BigDecimal productCount; + + @Column(name = "`product_price`") + @ApiModelProperty(value = "预算单价") + private BigDecimal productPrice; + + @Column(name = "`total_fee`") + @ApiModelProperty(value = "预算总价") + private BigDecimal totalFee; + + @Column(name = "`fee_source`") + @ApiModelProperty(value = "资金来源") + private String feeSource; + + @Column(name = "`project_name`") + @ApiModelProperty(value = "项目名称") + private String projectName; + + @Column(name = "`project_no`") + @ApiModelProperty(value = "项目编号") + private String projectNo; + + @Column(name = "`purchase_time`") + @ApiModelProperty(value = "预计采购时间") + private Timestamp purchaseTime; + + @Column(name = "`apply_time`") + @ApiModelProperty(value = "申请时间") + private Timestamp applyTime; + + @Column(name = "`remark`") + @ApiModelProperty(value = "备注") + private String remark; + + + public void copy(Purchase source){ + BeanUtil.copyProperties(source,this, CopyOptions.create().setIgnoreNullValue(true)); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Reimburse.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Reimburse.java new file mode 100644 index 0000000..5fde815 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Reimburse.java @@ -0,0 +1,105 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.domain; + +import lombok.Data; +import cn.hutool.core.bean.BeanUtil; +import io.swagger.annotations.ApiModelProperty; +import cn.hutool.core.bean.copier.CopyOptions; +import me.zhengjie.base.BaseEntity; +import me.zhengjie.exception.BadRequestException; + +import javax.persistence.*; +import javax.validation.constraints.*; +import java.sql.Timestamp; +import java.io.Serializable; + +/** +* @website https://eladmin.vip +* @description / +* @author author +* +**/ +@Entity +@Data +@Table(name="reimburse") +public class Reimburse extends BaseEntity implements Serializable { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "`reimburse_id`") + @ApiModelProperty(value = "ID") + private Long reimburseId; + + @ApiModelProperty(value = "申请人") + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + @Column(name = "`reason`",nullable = false) + @NotBlank + @ApiModelProperty(value = "申请事由") + private String reason; + + @Column(name = "`fee`",nullable = false) + @NotBlank + @ApiModelProperty(value = "申请金额") + private String fee; + + @Column(name = "`reimburse_type`",nullable = false) + @NotBlank + @ApiModelProperty(value = "报销类型") + private String reimburseType; + + @Column(name = "`fee_source`",nullable = false) + @NotBlank + @ApiModelProperty(value = "资金来源") + private String feeSource; + + @Column(name = "`business_fee_type`",nullable = false) + @ApiModelProperty(value = "业务费类型") + private String businessFeeType; + + @Column(name = "`project_name`") + @ApiModelProperty(value = "项目名称") + private String projectName; + + @Column(name = "`project_no`") + @ApiModelProperty(value = "项目编号") + private String projectNo; + + @Column(name = "`project_fee`") + @ApiModelProperty(value = "项目预算") + private String projectFee; + + @Column(name = "`reimburse_time`",nullable = false) + @NotNull + @ApiModelProperty(value = "花费产生时间") + private Timestamp reimburseTime; + + @Column(name = "`apply_time`",nullable = false) + @NotNull + @ApiModelProperty(value = "申请时间") + private Timestamp applyTime; + + @Column(name = "`remark`") + @ApiModelProperty(value = "备注") + private String remark; + + public void copy(Reimburse source){ + BeanUtil.copyProperties(source,this, CopyOptions.create().setIgnoreNullValue(true)); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Role.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Role.java new file mode 100644 index 0000000..d33d75c --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/Role.java @@ -0,0 +1,99 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.domain; + +import com.alibaba.fastjson.annotation.JSONField; +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; +import me.zhengjie.base.BaseEntity; +import me.zhengjie.utils.enums.DataScopeEnum; + +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.Objects; +import java.util.Set; + +/** + * 角色 + * @author Zheng Jie + * @date 2018-11-22 + */ +@Getter +@Setter +@Entity +@Table(name = "sys_role") +public class Role extends BaseEntity implements Serializable { + + @Id + @Column(name = "role_id") + @NotNull(groups = {Update.class}) + @GeneratedValue(strategy = GenerationType.IDENTITY) + @ApiModelProperty(value = "ID", hidden = true) + private Long id; + + @JSONField(serialize = false) + @ManyToMany(mappedBy = "roles") + @ApiModelProperty(value = "用户", hidden = true) + private Set users; + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable(name = "sys_roles_menus", + joinColumns = {@JoinColumn(name = "role_id",referencedColumnName = "role_id")}, + inverseJoinColumns = {@JoinColumn(name = "menu_id",referencedColumnName = "menu_id")}) + @ApiModelProperty(value = "菜单", hidden = true) + private Set menus; + + @ManyToMany + @JoinTable(name = "sys_roles_depts", + joinColumns = {@JoinColumn(name = "role_id",referencedColumnName = "role_id")}, + inverseJoinColumns = {@JoinColumn(name = "dept_id",referencedColumnName = "dept_id")}) + @ApiModelProperty(value = "部门", hidden = true) + private Set depts; + + @NotBlank + @ApiModelProperty(value = "名称", hidden = true) + private String name; + + @ApiModelProperty(value = "数据权限,全部 、 本级 、 自定义") + private String dataScope = DataScopeEnum.THIS_LEVEL.getValue(); + + @Column(name = "level") + @ApiModelProperty(value = "级别,数值越小,级别越大") + private Integer level = 3; + + @ApiModelProperty(value = "描述") + private String description; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Role role = (Role) o; + return Objects.equals(id, role.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/User.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/User.java new file mode 100644 index 0000000..357836a --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/User.java @@ -0,0 +1,129 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.domain; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; +import me.zhengjie.base.BaseEntity; +import javax.persistence.*; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.Date; +import java.util.Objects; +import java.util.Set; + +/** + * @author Zheng Jie + * @date 2018-11-22 + */ +@Entity +@Getter +@Setter +@Table(name="sys_user") +public class User extends BaseEntity implements Serializable { + + @Id + @Column(name = "user_id") + @NotNull(groups = Update.class) + @GeneratedValue(strategy = GenerationType.IDENTITY) + @ApiModelProperty(value = "ID", hidden = true) + private Long id; + + @ManyToMany(fetch = FetchType.EAGER) + @ApiModelProperty(value = "用户角色") + @JoinTable(name = "sys_users_roles", + joinColumns = {@JoinColumn(name = "user_id",referencedColumnName = "user_id")}, + inverseJoinColumns = {@JoinColumn(name = "role_id",referencedColumnName = "role_id")}) + private Set roles; + + @ManyToMany(fetch = FetchType.EAGER) + @ApiModelProperty(value = "用户岗位") + @JoinTable(name = "sys_users_jobs", + joinColumns = {@JoinColumn(name = "user_id",referencedColumnName = "user_id")}, + inverseJoinColumns = {@JoinColumn(name = "job_id",referencedColumnName = "job_id")}) + private Set jobs; + + @OneToOne + @JoinColumn(name = "dept_id") + @ApiModelProperty(value = "用户部门") + private Dept dept; + + @NotBlank + @Column(unique = true) + @ApiModelProperty(value = "用户名称") + private String username; + + @NotBlank + @ApiModelProperty(value = "用户昵称") + private String nickName; + + @Email + @NotBlank + @ApiModelProperty(value = "邮箱") + private String email; + + @NotBlank + @ApiModelProperty(value = "电话号码") + private String phone; + +// @NotBlank + @ApiModelProperty(value = "电话号码") + private String idNo; + + @ApiModelProperty(value = "用户性别") + private String gender; + + @ApiModelProperty(value = "头像真实名称",hidden = true) + private String avatarName; + + @ApiModelProperty(value = "头像存储的路径", hidden = true) + private String avatarPath; + + @ApiModelProperty(value = "密码") + private String password; + + @NotNull + @ApiModelProperty(value = "是否启用") + private Boolean enabled; + + @ApiModelProperty(value = "是否为admin账号", hidden = true) + private Boolean isAdmin = false; + + @Column(name = "pwd_reset_time") + @ApiModelProperty(value = "最后修改密码的时间", hidden = true) + private Date pwdResetTime; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + User user = (User) o; + return Objects.equals(id, user.id) && + Objects.equals(username, user.username); + } + + @Override + public int hashCode() { + return Objects.hash(id, username); + } +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/UserBank.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/UserBank.java new file mode 100644 index 0000000..06bf995 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/UserBank.java @@ -0,0 +1,73 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.domain; + +import lombok.Data; +import cn.hutool.core.bean.BeanUtil; +import io.swagger.annotations.ApiModelProperty; +import cn.hutool.core.bean.copier.CopyOptions; +import me.zhengjie.base.BaseEntity; + +import javax.persistence.*; +import javax.validation.constraints.*; +import java.sql.Timestamp; +import java.io.Serializable; + +/** +* @website https://eladmin.vip +* @description / +* @author author +* @date 2023-10-31 +**/ +@Entity +@Data +@Table(name="user_bank") +public class UserBank extends BaseEntity implements Serializable { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "`bank_id`") + @ApiModelProperty(value = "ID") + private Long bankId; + + @ApiModelProperty(value = "所属用户") + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + @Column(name = "`bank_no`",nullable = false) + @NotBlank + @ApiModelProperty(value = "银行卡号") + private String bankNo; + + @Column(name = "`bank_name`",nullable = false) + @NotBlank + @ApiModelProperty(value = "银行卡所属支行") + private String bankName; + + @Column(name = "`bank_use`") + @ApiModelProperty(value = "是否常用账号") + private String bankUse; + + @Column(name = "`remark`") + @ApiModelProperty(value = "备注") + private String remark; + + + public void copy(UserBank source){ + BeanUtil.copyProperties(source,this, CopyOptions.create().setIgnoreNullValue(true)); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/vo/MenuMetaVo.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/vo/MenuMetaVo.java new file mode 100644 index 0000000..647baa8 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/vo/MenuMetaVo.java @@ -0,0 +1,35 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import java.io.Serializable; + +/** + * @author Zheng Jie + * @date 2018-12-20 + */ +@Data +@AllArgsConstructor +public class MenuMetaVo implements Serializable { + + private String title; + + private String icon; + + private Boolean noCache; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/vo/MenuVo.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/vo/MenuVo.java new file mode 100644 index 0000000..e1efa5a --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/vo/MenuVo.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.domain.vo; + +import lombok.Data; +import java.io.Serializable; +import java.util.List; + +/** + * 构建前端路由时用到 + * @author Zheng Jie + * @date 2018-12-20 + */ +@Data +public class MenuVo implements Serializable { + + private String name; + + private String path; + + private Boolean hidden; + + private String redirect; + + private String component; + + private Boolean alwaysShow; + + private MenuMetaVo meta; + + private List children; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/vo/UserPassVo.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/vo/UserPassVo.java new file mode 100644 index 0000000..fccb100 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/domain/vo/UserPassVo.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.domain.vo; + +import lombok.Data; + +/** + * 修改密码的 Vo 类 + * @author Zheng Jie + * @date 2019年7月11日13:59:49 + */ +@Data +public class UserPassVo { + + private String oldPass; + + private String newPass; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/DeptRepository.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/DeptRepository.java new file mode 100644 index 0000000..9858322 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/DeptRepository.java @@ -0,0 +1,69 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.repository; + +import me.zhengjie.modules.system.domain.Dept; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import java.util.List; +import java.util.Set; + +/** +* @author Zheng Jie +* @date 2019-03-25 +*/ +public interface DeptRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * 根据 PID 查询 + * @param id pid + * @return / + */ + List findByPid(Long id); + + /** + * 获取顶级部门 + * @return / + */ + List findByPidIsNull(); + + /** + * 根据角色ID 查询 + * @param roleId 角色ID + * @return / + */ + @Query(value = "select d.* from sys_dept d, sys_roles_depts r where " + + "d.dept_id = r.dept_id and r.role_id = ?1", nativeQuery = true) + Set findByRoleId(Long roleId); + + /** + * 判断是否存在子节点 + * @param pid / + * @return / + */ + int countByPid(Long pid); + + /** + * 根据ID更新sub_count + * @param count / + * @param id / + */ + @Modifying + @Query(value = " update sys_dept set sub_count = ?1 where dept_id = ?2 ",nativeQuery = true) + void updateSubCntById(Integer count, Long id); +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/DictDetailRepository.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/DictDetailRepository.java new file mode 100644 index 0000000..d80b970 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/DictDetailRepository.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.repository; + +import me.zhengjie.modules.system.domain.DictDetail; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.util.List; + +/** +* @author Zheng Jie +* @date 2019-04-10 +*/ +public interface DictDetailRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * 根据字典名称查询 + * @param name / + * @return / + */ + List findByDictName(String name); +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/DictRepository.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/DictRepository.java new file mode 100644 index 0000000..f09b6d1 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/DictRepository.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.repository; + +import me.zhengjie.modules.system.domain.Dict; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.util.List; +import java.util.Set; + +/** +* @author Zheng Jie +* @date 2019-04-10 +*/ +public interface DictRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * 删除 + * @param ids / + */ + void deleteByIdIn(Set ids); + + /** + * 查询 + * @param ids / + * @return / + */ + List findByIdIn(Set ids); +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/JobRepository.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/JobRepository.java new file mode 100644 index 0000000..e39ebf0 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/JobRepository.java @@ -0,0 +1,42 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.repository; + +import me.zhengjie.modules.system.domain.Job; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.util.Set; + +/** +* @author Zheng Jie +* @date 2019-03-29 +*/ +public interface JobRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * 根据名称查询 + * @param name 名称 + * @return / + */ + Job findByName(String name); + + /** + * 根据Id删除 + * @param ids / + */ + void deleteAllByIdIn(Set ids); +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/MenuRepository.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/MenuRepository.java new file mode 100644 index 0000000..75fd7ff --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/MenuRepository.java @@ -0,0 +1,85 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.repository; + +import me.zhengjie.modules.system.domain.Menu; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * @author Zheng Jie + * @date 2018-12-17 + */ +public interface MenuRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * 根据菜单标题查询 + * @param title 菜单标题 + * @return / + */ + Menu findByTitle(String title); + + /** + * 根据组件名称查询 + * @param name 组件名称 + * @return / + */ + Menu findByComponentName(String name); + + /** + * 根据菜单的 PID 查询 + * @param pid / + * @return / + */ + List findByPidOrderByMenuSort(long pid); + + /** + * 查询顶级菜单 + * @return / + */ + List findByPidIsNullOrderByMenuSort(); + + /** + * 根据角色ID与菜单类型查询菜单 + * @param roleIds roleIDs + * @param type 类型 + * @return / + */ + @Query(value = "SELECT m.* FROM sys_menu m, sys_roles_menus r WHERE " + + "m.menu_id = r.menu_id AND r.role_id IN ?1 AND type != ?2 order by m.menu_sort asc",nativeQuery = true) + LinkedHashSet findByRoleIdsAndTypeNot(Set roleIds, int type); + + /** + * 获取节点数量 + * @param id / + * @return / + */ + int countByPid(Long id); + + /** + * 更新节点数目 + * @param count / + * @param menuId / + */ + @Modifying + @Query(value = " update sys_menu set sub_count = ?1 where menu_id = ?2 ",nativeQuery = true) + void updateSubCntById(int count, Long menuId); +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/PurchaseRepository.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/PurchaseRepository.java new file mode 100644 index 0000000..ce33a89 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/PurchaseRepository.java @@ -0,0 +1,28 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.repository; + +import me.zhengjie.modules.system.domain.Purchase; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** +* @website https://eladmin.vip +* @author author +* +**/ +public interface PurchaseRepository extends JpaRepository, JpaSpecificationExecutor { +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/ReimburseRepository.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/ReimburseRepository.java new file mode 100644 index 0000000..81f5151 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/ReimburseRepository.java @@ -0,0 +1,28 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.repository; + +import me.zhengjie.modules.system.domain.Reimburse; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** +* @website https://eladmin.vip +* @author author +* +**/ +public interface ReimburseRepository extends JpaRepository, JpaSpecificationExecutor { +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/RoleRepository.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/RoleRepository.java new file mode 100644 index 0000000..8e76cc5 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/RoleRepository.java @@ -0,0 +1,80 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.repository; + +import me.zhengjie.modules.system.domain.Role; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; +import java.util.Set; + +/** + * @author Zheng Jie + * @date 2018-12-03 + */ +public interface RoleRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * 根据名称查询 + * @param name / + * @return / + */ + Role findByName(String name); + + /** + * 删除多个角色 + * @param ids / + */ + void deleteAllByIdIn(Set ids); + + /** + * 根据用户ID查询 + * @param id 用户ID + * @return / + */ + @Query(value = "SELECT r.* FROM sys_role r, sys_users_roles u WHERE " + + "r.role_id = u.role_id AND u.user_id = ?1",nativeQuery = true) + Set findByUserId(Long id); + + /** + * 解绑角色菜单 + * @param id 菜单ID + */ + @Modifying + @Query(value = "delete from sys_roles_menus where menu_id = ?1",nativeQuery = true) + void untiedMenu(Long id); + + /** + * 根据部门查询 + * @param deptIds / + * @return / + */ + @Query(value = "select count(1) from sys_role r, sys_roles_depts d where " + + "r.role_id = d.role_id and d.dept_id in ?1",nativeQuery = true) + int countByDepts(Set deptIds); + + /** + * 根据菜单Id查询 + * @param menuIds / + * @return / + */ + @Query(value = "SELECT r.* FROM sys_role r, sys_roles_menus m WHERE " + + "r.role_id = m.role_id AND m.menu_id in ?1",nativeQuery = true) + List findInMenuId(List menuIds); +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/UserBankRepository.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/UserBankRepository.java new file mode 100644 index 0000000..4eb7a17 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/UserBankRepository.java @@ -0,0 +1,33 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.repository; + +import me.zhengjie.modules.system.domain.User; +import me.zhengjie.modules.system.domain.UserBank; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.util.List; + +/** +* @website https://eladmin.vip +* @author author +* @date 2023-10-31 +**/ +public interface UserBankRepository extends JpaRepository, JpaSpecificationExecutor { + + UserBank findByBankUseAndUser(String bankUse, User user); +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/UserRepository.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/UserRepository.java new file mode 100644 index 0000000..f7583b0 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/repository/UserRepository.java @@ -0,0 +1,139 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.repository; + +import me.zhengjie.modules.system.domain.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import java.util.Date; +import java.util.List; +import java.util.Set; + +/** + * @author Zheng Jie + * @date 2018-11-22 + */ +public interface UserRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * 根据用户名查询 + * @param username 用户名 + * @return / + */ + User findByUsername(String username); + + /** + * 根据邮箱查询 + * @param email 邮箱 + * @return / + */ + User findByEmail(String email); + + /** + * 根据手机号查询 + * @param phone 手机号 + * @return / + */ + User findByPhone(String phone); + + /** + * 修改密码 + * @param username 用户名 + * @param pass 密码 + * @param lastPasswordResetTime / + */ + @Modifying + @Query(value = "update sys_user set password = ?2 , pwd_reset_time = ?3 where username = ?1",nativeQuery = true) + void updatePass(String username, String pass, Date lastPasswordResetTime); + + /** + * 修改邮箱 + * @param username 用户名 + * @param email 邮箱 + */ + @Modifying + @Query(value = "update sys_user set email = ?2 where username = ?1",nativeQuery = true) + void updateEmail(String username, String email); + + /** + * 根据角色查询用户 + * @param roleId / + * @return / + */ + @Query(value = "SELECT u.* FROM sys_user u, sys_users_roles r WHERE" + + " u.user_id = r.user_id AND r.role_id = ?1", nativeQuery = true) + List findByRoleId(Long roleId); + + /** + * 根据角色中的部门查询 + * @param deptId / + * @return / + */ + @Query(value = "SELECT u.* FROM sys_user u, sys_users_roles r, sys_roles_depts d WHERE " + + "u.user_id = r.user_id AND r.role_id = d.role_id AND d.dept_id = ?1 group by u.user_id", nativeQuery = true) + List findByRoleDeptId(Long deptId); + + /** + * 根据菜单查询 + * @param id 菜单ID + * @return / + */ + @Query(value = "SELECT u.* FROM sys_user u, sys_users_roles ur, sys_roles_menus rm WHERE\n" + + "u.user_id = ur.user_id AND ur.role_id = rm.role_id AND rm.menu_id = ?1 group by u.user_id", nativeQuery = true) + List findByMenuId(Long id); + + /** + * 根据Id删除 + * @param ids / + */ + void deleteAllByIdIn(Set ids); + + /** + * 根据岗位查询 + * @param ids / + * @return / + */ + @Query(value = "SELECT count(1) FROM sys_user u, sys_users_jobs j WHERE u.user_id = j.user_id AND j.job_id IN ?1", nativeQuery = true) + int countByJobs(Set ids); + + /** + * 根据部门查询 + * @param deptIds / + * @return / + */ + @Query(value = "SELECT count(1) FROM sys_user u WHERE u.dept_id IN ?1", nativeQuery = true) + int countByDepts(Set deptIds); + + /** + * 根据角色查询 + * @param ids / + * @return / + */ + @Query(value = "SELECT count(1) FROM sys_user u, sys_users_roles r WHERE " + + "u.user_id = r.user_id AND r.role_id in ?1", nativeQuery = true) + int countByRoles(Set ids); + + /** + * 重置密码 + * @param ids 、 + * @param pwd 、 + */ + @Modifying + @Query(value = "update sys_user set password = ?2 where user_id in ?1",nativeQuery = true) + void resetPwd(Set ids, String pwd); +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/DeptController.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/DeptController.java new file mode 100644 index 0000000..ebeb260 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/DeptController.java @@ -0,0 +1,129 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.rest; + +import cn.hutool.core.collection.CollectionUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import me.zhengjie.annotation.Log; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.modules.system.domain.Dept; +import me.zhengjie.modules.system.service.DeptService; +import me.zhengjie.modules.system.service.dto.DeptDto; +import me.zhengjie.modules.system.service.dto.DeptQueryCriteria; +import me.zhengjie.utils.PageResult; +import me.zhengjie.utils.PageUtil; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import javax.servlet.http.HttpServletResponse; +import java.util.*; +import java.util.stream.Collectors; + +/** +* @author Zheng Jie +* @date 2019-03-25 +*/ +@RestController +@RequiredArgsConstructor +@Api(tags = "系统:部门管理") +@RequestMapping("/api/dept") +public class DeptController { + + private final DeptService deptService; + private static final String ENTITY_NAME = "dept"; + + @ApiOperation("导出部门数据") + @GetMapping(value = "/download") + @PreAuthorize("@el.check('dept:list')") + public void exportDept(HttpServletResponse response, DeptQueryCriteria criteria) throws Exception { + deptService.download(deptService.queryAll(criteria, false), response); + } + + @ApiOperation("查询部门") + @GetMapping + @PreAuthorize("@el.check('user:list','dept:list')") + public ResponseEntity> queryDept(DeptQueryCriteria criteria) throws Exception { + List depts = deptService.queryAll(criteria, true); + return new ResponseEntity<>(PageUtil.toPage(depts, depts.size()),HttpStatus.OK); + } + + @ApiOperation("查询部门:根据ID获取同级与上级数据") + @PostMapping("/superior") + @PreAuthorize("@el.check('user:list','dept:list')") + public ResponseEntity getDeptSuperior(@RequestBody List ids, + @RequestParam(defaultValue = "false") Boolean exclude) { + Set deptSet = new LinkedHashSet<>(); + for (Long id : ids) { + DeptDto deptDto = deptService.findById(id); + List depts = deptService.getSuperior(deptDto, new ArrayList<>()); + if(exclude){ + for (DeptDto dept : depts) { + if(dept.getId().equals(deptDto.getPid())) { + dept.setSubCount(dept.getSubCount() - 1); + } + } + // 编辑部门时不显示自己以及自己下级的数据,避免出现PID数据环形问题 + depts = depts.stream().filter(i -> !ids.contains(i.getId())).collect(Collectors.toList()); + } + deptSet.addAll(depts); + } + return new ResponseEntity<>(deptService.buildTree(new ArrayList<>(deptSet)),HttpStatus.OK); + } + + @Log("新增部门") + @ApiOperation("新增部门") + @PostMapping + @PreAuthorize("@el.check('dept:add')") + public ResponseEntity createDept(@Validated @RequestBody Dept resources){ + if (resources.getId() != null) { + throw new BadRequestException("A new "+ ENTITY_NAME +" cannot already have an ID"); + } + deptService.create(resources); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @Log("修改部门") + @ApiOperation("修改部门") + @PutMapping + @PreAuthorize("@el.check('dept:edit')") + public ResponseEntity updateDept(@Validated(Dept.Update.class) @RequestBody Dept resources){ + deptService.update(resources); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @Log("删除部门") + @ApiOperation("删除部门") + @DeleteMapping + @PreAuthorize("@el.check('dept:del')") + public ResponseEntity deleteDept(@RequestBody Set ids){ + Set deptDtos = new HashSet<>(); + for (Long id : ids) { + List deptList = deptService.findByPid(id); + deptDtos.add(deptService.findById(id)); + if(CollectionUtil.isNotEmpty(deptList)){ + deptDtos = deptService.getDeleteDepts(deptList, deptDtos); + } + } + // 验证是否被角色或用户关联 + deptService.verification(deptDtos); + deptService.delete(deptDtos); + return new ResponseEntity<>(HttpStatus.OK); + } +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/DictController.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/DictController.java new file mode 100644 index 0000000..6161f54 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/DictController.java @@ -0,0 +1,103 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import me.zhengjie.annotation.Log; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.modules.system.domain.Dict; +import me.zhengjie.modules.system.service.DictService; +import me.zhengjie.modules.system.service.dto.DictDto; +import me.zhengjie.modules.system.service.dto.DictQueryCriteria; +import me.zhengjie.utils.PageResult; +import me.zhengjie.utils.PageUtil; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +/** +* @author Zheng Jie +* @date 2019-04-10 +*/ +@RestController +@RequiredArgsConstructor +@Api(tags = "系统:字典管理") +@RequestMapping("/api/dict") +public class DictController { + + private final DictService dictService; + private static final String ENTITY_NAME = "dict"; + + @ApiOperation("导出字典数据") + @GetMapping(value = "/download") + @PreAuthorize("@el.check('dict:list')") + public void exportDict(HttpServletResponse response, DictQueryCriteria criteria) throws IOException { + dictService.download(dictService.queryAll(criteria), response); + } + + @ApiOperation("查询字典") + @GetMapping(value = "/all") + @PreAuthorize("@el.check('dict:list')") + public ResponseEntity> queryAllDict(){ + return new ResponseEntity<>(dictService.queryAll(new DictQueryCriteria()),HttpStatus.OK); + } + + @ApiOperation("查询字典") + @GetMapping + @PreAuthorize("@el.check('dict:list')") + public ResponseEntity> queryDict(DictQueryCriteria resources, Pageable pageable){ + return new ResponseEntity<>(dictService.queryAll(resources,pageable),HttpStatus.OK); + } + + @Log("新增字典") + @ApiOperation("新增字典") + @PostMapping + @PreAuthorize("@el.check('dict:add')") + public ResponseEntity createDict(@Validated @RequestBody Dict resources){ + if (resources.getId() != null) { + throw new BadRequestException("A new "+ ENTITY_NAME +" cannot already have an ID"); + } + dictService.create(resources); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @Log("修改字典") + @ApiOperation("修改字典") + @PutMapping + @PreAuthorize("@el.check('dict:edit')") + public ResponseEntity updateDict(@Validated(Dict.Update.class) @RequestBody Dict resources){ + dictService.update(resources); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @Log("删除字典") + @ApiOperation("删除字典") + @DeleteMapping + @PreAuthorize("@el.check('dict:del')") + public ResponseEntity deleteDict(@RequestBody Set ids){ + dictService.delete(ids); + return new ResponseEntity<>(HttpStatus.OK); + } +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/DictDetailController.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/DictDetailController.java new file mode 100644 index 0000000..3cea7a1 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/DictDetailController.java @@ -0,0 +1,100 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import me.zhengjie.annotation.Log; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.modules.system.domain.DictDetail; +import me.zhengjie.modules.system.service.DictDetailService; +import me.zhengjie.modules.system.service.dto.DictDetailDto; +import me.zhengjie.modules.system.service.dto.DictDetailQueryCriteria; +import me.zhengjie.utils.PageResult; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** +* @author Zheng Jie +* @date 2019-04-10 +*/ +@RestController +@RequiredArgsConstructor +@Api(tags = "系统:字典详情管理") +@RequestMapping("/api/dictDetail") +public class DictDetailController { + + private final DictDetailService dictDetailService; + private static final String ENTITY_NAME = "dictDetail"; + + @ApiOperation("查询字典详情") + @GetMapping + public ResponseEntity> queryDictDetail(DictDetailQueryCriteria criteria, + @PageableDefault(sort = {"dictSort"}, direction = Sort.Direction.ASC) Pageable pageable){ + return new ResponseEntity<>(dictDetailService.queryAll(criteria,pageable),HttpStatus.OK); + } + + @ApiOperation("查询多个字典详情") + @GetMapping(value = "/map") + public ResponseEntity getDictDetailMaps(@RequestParam String dictName){ + String[] names = dictName.split("[,,]"); + Map> dictMap = new HashMap<>(16); + for (String name : names) { + dictMap.put(name, dictDetailService.getDictByName(name)); + } + return new ResponseEntity<>(dictMap, HttpStatus.OK); + } + + @Log("新增字典详情") + @ApiOperation("新增字典详情") + @PostMapping + @PreAuthorize("@el.check('dict:add')") + public ResponseEntity createDictDetail(@Validated @RequestBody DictDetail resources){ + if (resources.getId() != null) { + throw new BadRequestException("A new "+ ENTITY_NAME +" cannot already have an ID"); + } + dictDetailService.create(resources); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @Log("修改字典详情") + @ApiOperation("修改字典详情") + @PutMapping + @PreAuthorize("@el.check('dict:edit')") + public ResponseEntity updateDictDetail(@Validated(DictDetail.Update.class) @RequestBody DictDetail resources){ + dictDetailService.update(resources); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @Log("删除字典详情") + @ApiOperation("删除字典详情") + @DeleteMapping(value = "/{id}") + @PreAuthorize("@el.check('dict:del')") + public ResponseEntity deleteDictDetail(@PathVariable Long id){ + dictDetailService.delete(id); + return new ResponseEntity<>(HttpStatus.OK); + } +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/JobController.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/JobController.java new file mode 100644 index 0000000..fa10176 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/JobController.java @@ -0,0 +1,96 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import me.zhengjie.annotation.Log; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.modules.system.domain.Job; +import me.zhengjie.modules.system.service.JobService; +import me.zhengjie.modules.system.service.dto.JobDto; +import me.zhengjie.modules.system.service.dto.JobQueryCriteria; +import me.zhengjie.utils.PageResult; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Set; + +/** +* @author Zheng Jie +* @date 2019-03-29 +*/ +@RestController +@RequiredArgsConstructor +@Api(tags = "系统:岗位管理") +@RequestMapping("/api/job") +public class JobController { + + private final JobService jobService; + private static final String ENTITY_NAME = "job"; + + @ApiOperation("导出岗位数据") + @GetMapping(value = "/download") + @PreAuthorize("@el.check('job:list')") + public void exportJob(HttpServletResponse response, JobQueryCriteria criteria) throws IOException { + jobService.download(jobService.queryAll(criteria), response); + } + + @ApiOperation("查询岗位") + @GetMapping + @PreAuthorize("@el.check('job:list','user:list')") + public ResponseEntity> queryJob(JobQueryCriteria criteria, Pageable pageable){ + return new ResponseEntity<>(jobService.queryAll(criteria, pageable),HttpStatus.OK); + } + + @Log("新增岗位") + @ApiOperation("新增岗位") + @PostMapping + @PreAuthorize("@el.check('job:add')") + public ResponseEntity createJob(@Validated @RequestBody Job resources){ + if (resources.getId() != null) { + throw new BadRequestException("A new "+ ENTITY_NAME +" cannot already have an ID"); + } + jobService.create(resources); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @Log("修改岗位") + @ApiOperation("修改岗位") + @PutMapping + @PreAuthorize("@el.check('job:edit')") + public ResponseEntity updateJob(@Validated(Job.Update.class) @RequestBody Job resources){ + jobService.update(resources); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @Log("删除岗位") + @ApiOperation("删除岗位") + @DeleteMapping + @PreAuthorize("@el.check('job:del')") + public ResponseEntity deleteJob(@RequestBody Set ids){ + // 验证是否被用户关联 + jobService.verification(ids); + jobService.delete(ids); + return new ResponseEntity<>(HttpStatus.OK); + } +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/LimitController.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/LimitController.java new file mode 100644 index 0000000..02db642 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/LimitController.java @@ -0,0 +1,47 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import me.zhengjie.annotation.Limit; +import me.zhengjie.annotation.rest.AnonymousGetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author / + * 接口限流测试类 + */ +@RestController +@RequestMapping("/api/limit") +@Api(tags = "系统:限流测试管理") +public class LimitController { + + private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger(); + + /** + * 测试限流注解,下面配置说明该接口 60秒内最多只能访问 10次,保存到redis的键名为 limit_test, + */ + @AnonymousGetMapping + @ApiOperation("测试") + @Limit(key = "test", period = 60, count = 10, name = "testLimit", prefix = "limit") + public int testLimit() { + return ATOMIC_INTEGER.incrementAndGet(); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/MenuController.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/MenuController.java new file mode 100644 index 0000000..18084f1 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/MenuController.java @@ -0,0 +1,157 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.rest; + +import cn.hutool.core.collection.CollectionUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import me.zhengjie.annotation.Log; +import me.zhengjie.modules.system.domain.Menu; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.modules.system.domain.vo.MenuVo; +import me.zhengjie.modules.system.service.MenuService; +import me.zhengjie.modules.system.service.dto.DeptDto; +import me.zhengjie.modules.system.service.dto.MenuDto; +import me.zhengjie.modules.system.service.dto.MenuQueryCriteria; +import me.zhengjie.modules.system.service.mapstruct.MenuMapper; +import me.zhengjie.utils.PageResult; +import me.zhengjie.utils.PageUtil; +import me.zhengjie.utils.SecurityUtils; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import javax.servlet.http.HttpServletResponse; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author Zheng Jie + * @date 2018-12-03 + */ +@RestController +@RequiredArgsConstructor +@Api(tags = "系统:菜单管理") +@RequestMapping("/api/menus") +public class MenuController { + + private final MenuService menuService; + private final MenuMapper menuMapper; + private static final String ENTITY_NAME = "menu"; + + @ApiOperation("导出菜单数据") + @GetMapping(value = "/download") + @PreAuthorize("@el.check('menu:list')") + public void exportMenu(HttpServletResponse response, MenuQueryCriteria criteria) throws Exception { + menuService.download(menuService.queryAll(criteria, false), response); + } + + @GetMapping(value = "/build") + @ApiOperation("获取前端所需菜单") + public ResponseEntity> buildMenus(){ + List menuDtoList = menuService.findByUser(SecurityUtils.getCurrentUserId()); + List menus = menuService.buildTree(menuDtoList); + return new ResponseEntity<>(menuService.buildMenus(menus),HttpStatus.OK); + } + + @ApiOperation("返回全部的菜单") + @GetMapping(value = "/lazy") + @PreAuthorize("@el.check('menu:list','roles:list')") + public ResponseEntity> queryAllMenu(@RequestParam Long pid){ + return new ResponseEntity<>(menuService.getMenus(pid),HttpStatus.OK); + } + + @ApiOperation("根据菜单ID返回所有子节点ID,包含自身ID") + @GetMapping(value = "/child") + @PreAuthorize("@el.check('menu:list','roles:list')") + public ResponseEntity childMenu(@RequestParam Long id){ + Set menuSet = new HashSet<>(); + List menuList = menuService.getMenus(id); + menuSet.add(menuService.findOne(id)); + menuSet = menuService.getChildMenus(menuMapper.toEntity(menuList), menuSet); + Set ids = menuSet.stream().map(Menu::getId).collect(Collectors.toSet()); + return new ResponseEntity<>(ids,HttpStatus.OK); + } + + @GetMapping + @ApiOperation("查询菜单") + @PreAuthorize("@el.check('menu:list')") + public ResponseEntity> queryMenu(MenuQueryCriteria criteria) throws Exception { + List menuDtoList = menuService.queryAll(criteria, true); + return new ResponseEntity<>(PageUtil.toPage(menuDtoList, menuDtoList.size()),HttpStatus.OK); + } + + @ApiOperation("查询菜单:根据ID获取同级与上级数据") + @PostMapping("/superior") + @PreAuthorize("@el.check('menu:list')") + public ResponseEntity> getMenuSuperior(@RequestBody List ids) { + Set menuDtos = new LinkedHashSet<>(); + if(CollectionUtil.isNotEmpty(ids)){ + for (Long id : ids) { + MenuDto menuDto = menuService.findById(id); + List menuDtoList = menuService.getSuperior(menuDto, new ArrayList<>()); + for (MenuDto menu : menuDtoList) { + if(menu.getId().equals(menuDto.getPid())) { + menu.setSubCount(menu.getSubCount() - 1); + } + } + menuDtos.addAll(menuDtoList); + } + // 编辑菜单时不显示自己以及自己下级的数据,避免出现PID数据环形问题 + menuDtos = menuDtos.stream().filter(i -> !ids.contains(i.getId())).collect(Collectors.toSet()); + return new ResponseEntity<>(menuService.buildTree(new ArrayList<>(menuDtos)),HttpStatus.OK); + } + return new ResponseEntity<>(menuService.getMenus(null),HttpStatus.OK); + } + + @Log("新增菜单") + @ApiOperation("新增菜单") + @PostMapping + @PreAuthorize("@el.check('menu:add')") + public ResponseEntity createMenu(@Validated @RequestBody Menu resources){ + if (resources.getId() != null) { + throw new BadRequestException("A new "+ ENTITY_NAME +" cannot already have an ID"); + } + menuService.create(resources); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @Log("修改菜单") + @ApiOperation("修改菜单") + @PutMapping + @PreAuthorize("@el.check('menu:edit')") + public ResponseEntity updateMenu(@Validated(Menu.Update.class) @RequestBody Menu resources){ + menuService.update(resources); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @Log("删除菜单") + @ApiOperation("删除菜单") + @DeleteMapping + @PreAuthorize("@el.check('menu:del')") + public ResponseEntity deleteMenu(@RequestBody Set ids){ + Set menuSet = new HashSet<>(); + for (Long id : ids) { + List menuList = menuService.getMenus(id); + menuSet.add(menuService.findOne(id)); + menuSet = menuService.getChildMenus(menuMapper.toEntity(menuList), menuSet); + } + menuService.delete(menuSet); + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/MonitorController.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/MonitorController.java new file mode 100644 index 0000000..bb0e39b --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/MonitorController.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import me.zhengjie.modules.system.service.MonitorService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +/** + * @author Zheng Jie + * @date 2020-05-02 + */ +@RestController +@RequiredArgsConstructor +@Api(tags = "系统-服务监控管理") +@RequestMapping("/api/monitor") +public class MonitorController { + + private final MonitorService serverService; + + @GetMapping + @ApiOperation("查询服务监控") + @PreAuthorize("@el.check('monitor:list')") + public ResponseEntity queryMonitor(){ + return new ResponseEntity<>(serverService.getServers(),HttpStatus.OK); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/PurchaseController.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/PurchaseController.java new file mode 100644 index 0000000..3202e0d --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/PurchaseController.java @@ -0,0 +1,90 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.rest; + +import me.zhengjie.annotation.Log; +import me.zhengjie.modules.system.domain.Purchase; +import me.zhengjie.modules.system.service.PurchaseService; +import me.zhengjie.modules.system.service.dto.PurchaseQueryCriteria; +import org.springframework.data.domain.Pageable; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import io.swagger.annotations.*; +import java.io.IOException; +import javax.servlet.http.HttpServletResponse; +import me.zhengjie.utils.PageResult; +import me.zhengjie.modules.system.service.dto.PurchaseDto; + +/** +* @website https://eladmin.vip +* @author author +* +**/ +@RestController +@RequiredArgsConstructor +@Api(tags = "经费支出管理") +@RequestMapping("/api/purchase") +public class PurchaseController { + + private final PurchaseService purchaseService; + + @Log("导出数据") + @ApiOperation("导出数据") + @GetMapping(value = "/download") + @PreAuthorize("@el.check('purchase:list')") + public void exportPurchase(HttpServletResponse response, PurchaseQueryCriteria criteria) throws IOException { + purchaseService.download(purchaseService.queryAll(criteria), response); + } + + @GetMapping + @Log("查询经费支出") + @ApiOperation("查询经费支出") + @PreAuthorize("@el.check('purchase:list')") + public ResponseEntity> queryPurchase(PurchaseQueryCriteria criteria, Pageable pageable){ + return new ResponseEntity<>(purchaseService.queryAll(criteria,pageable),HttpStatus.OK); + } + + @PostMapping + @Log("新增经费支出") + @ApiOperation("新增经费支出") + @PreAuthorize("@el.check('purchase:add')") + public ResponseEntity createPurchase(@Validated @RequestBody Purchase resources){ + purchaseService.create(resources); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @PutMapping + @Log("修改经费支出") + @ApiOperation("修改经费支出") + @PreAuthorize("@el.check('purchase:edit')") + public ResponseEntity updatePurchase(@Validated @RequestBody Purchase resources){ + purchaseService.update(resources); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @DeleteMapping + @Log("删除经费支出") + @ApiOperation("删除经费支出") + @PreAuthorize("@el.check('purchase:del')") + public ResponseEntity deletePurchase(@RequestBody Long[] ids) { + purchaseService.deleteAll(ids); + return new ResponseEntity<>(HttpStatus.OK); + } +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/ReimburseController.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/ReimburseController.java new file mode 100644 index 0000000..96ce25d --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/ReimburseController.java @@ -0,0 +1,90 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.rest; + +import me.zhengjie.annotation.Log; +import me.zhengjie.modules.system.domain.Reimburse; +import me.zhengjie.modules.system.service.ReimburseService; +import me.zhengjie.modules.system.service.dto.ReimburseQueryCriteria; +import org.springframework.data.domain.Pageable; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import io.swagger.annotations.*; +import java.io.IOException; +import javax.servlet.http.HttpServletResponse; +import me.zhengjie.utils.PageResult; +import me.zhengjie.modules.system.service.dto.ReimburseDto; + +/** +* @website https://eladmin.vip +* @author author +* +**/ +@RestController +@RequiredArgsConstructor +@Api(tags = "报销单管理") +@RequestMapping("/api/reimburse") +public class ReimburseController { + + private final ReimburseService reimburseService; + + @Log("导出数据") + @ApiOperation("导出数据") + @GetMapping(value = "/download") + @PreAuthorize("@el.check('reimburse:list')") + public void exportReimburse(HttpServletResponse response, ReimburseQueryCriteria criteria) throws IOException { + reimburseService.download(reimburseService.queryAll(criteria), response); + } + + @GetMapping + @Log("查询报销单") + @ApiOperation("查询报销单") + @PreAuthorize("@el.check('reimburse:list')") + public ResponseEntity> queryReimburse(ReimburseQueryCriteria criteria, Pageable pageable){ + return new ResponseEntity<>(reimburseService.queryAll(criteria,pageable),HttpStatus.OK); + } + + @PostMapping + @Log("新增报销单") + @ApiOperation("新增报销单") + @PreAuthorize("@el.check('reimburse:add')") + public ResponseEntity createReimburse(@Validated @RequestBody Reimburse resources){ + reimburseService.create(resources); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @PutMapping + @Log("修改报销单") + @ApiOperation("修改报销单") + @PreAuthorize("@el.check('reimburse:edit')") + public ResponseEntity updateReimburse(@Validated @RequestBody Reimburse resources){ + reimburseService.update(resources); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @DeleteMapping + @Log("删除报销单") + @ApiOperation("删除报销单") + @PreAuthorize("@el.check('reimburse:del')") + public ResponseEntity deleteReimburse(@RequestBody Long[] ids) { + reimburseService.deleteAll(ids); + return new ResponseEntity<>(HttpStatus.OK); + } +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/RoleController.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/RoleController.java new file mode 100644 index 0000000..6988432 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/RoleController.java @@ -0,0 +1,155 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.rest; + +import cn.hutool.core.lang.Dict; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import me.zhengjie.annotation.Log; +import me.zhengjie.modules.system.domain.Role; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.modules.system.service.RoleService; +import me.zhengjie.modules.system.service.dto.RoleDto; +import me.zhengjie.modules.system.service.dto.RoleQueryCriteria; +import me.zhengjie.modules.system.service.dto.RoleSmallDto; +import me.zhengjie.utils.PageResult; +import me.zhengjie.utils.SecurityUtils; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author Zheng Jie + * @date 2018-12-03 + */ +@RestController +@RequiredArgsConstructor +@Api(tags = "系统:角色管理") +@RequestMapping("/api/roles") +public class RoleController { + + private final RoleService roleService; + + private static final String ENTITY_NAME = "role"; + + @ApiOperation("获取单个role") + @GetMapping(value = "/{id}") + @PreAuthorize("@el.check('roles:list')") + public ResponseEntity findRoleById(@PathVariable Long id){ + return new ResponseEntity<>(roleService.findById(id), HttpStatus.OK); + } + + @ApiOperation("导出角色数据") + @GetMapping(value = "/download") + @PreAuthorize("@el.check('role:list')") + public void exportRole(HttpServletResponse response, RoleQueryCriteria criteria) throws IOException { + roleService.download(roleService.queryAll(criteria), response); + } + + @ApiOperation("返回全部的角色") + @GetMapping(value = "/all") + @PreAuthorize("@el.check('roles:list','user:add','user:edit')") + public ResponseEntity> queryAllRole(){ + return new ResponseEntity<>(roleService.queryAll(),HttpStatus.OK); + } + + @ApiOperation("查询角色") + @GetMapping + @PreAuthorize("@el.check('roles:list')") + public ResponseEntity> queryRole(RoleQueryCriteria criteria, Pageable pageable){ + return new ResponseEntity<>(roleService.queryAll(criteria,pageable),HttpStatus.OK); + } + + @ApiOperation("获取用户级别") + @GetMapping(value = "/level") + public ResponseEntity getRoleLevel(){ + return new ResponseEntity<>(Dict.create().set("level", getLevels(null)),HttpStatus.OK); + } + + @Log("新增角色") + @ApiOperation("新增角色") + @PostMapping + @PreAuthorize("@el.check('roles:add')") + public ResponseEntity createRole(@Validated @RequestBody Role resources){ + if (resources.getId() != null) { + throw new BadRequestException("A new "+ ENTITY_NAME +" cannot already have an ID"); + } + getLevels(resources.getLevel()); + roleService.create(resources); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @Log("修改角色") + @ApiOperation("修改角色") + @PutMapping + @PreAuthorize("@el.check('roles:edit')") + public ResponseEntity updateRole(@Validated(Role.Update.class) @RequestBody Role resources){ + getLevels(resources.getLevel()); + roleService.update(resources); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @Log("修改角色菜单") + @ApiOperation("修改角色菜单") + @PutMapping(value = "/menu") + @PreAuthorize("@el.check('roles:edit')") + public ResponseEntity updateRoleMenu(@RequestBody Role resources){ + RoleDto role = roleService.findById(resources.getId()); + getLevels(role.getLevel()); + roleService.updateMenu(resources,role); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @Log("删除角色") + @ApiOperation("删除角色") + @DeleteMapping + @PreAuthorize("@el.check('roles:del')") + public ResponseEntity deleteRole(@RequestBody Set ids){ + for (Long id : ids) { + RoleDto role = roleService.findById(id); + getLevels(role.getLevel()); + } + // 验证是否被用户关联 + roleService.verification(ids); + roleService.delete(ids); + return new ResponseEntity<>(HttpStatus.OK); + } + + /** + * 获取用户的角色级别 + * @return / + */ + private int getLevels(Integer level){ + List levels = roleService.findByUsersId(SecurityUtils.getCurrentUserId()).stream().map(RoleSmallDto::getLevel).collect(Collectors.toList()); + int min = Collections.min(levels); + if(level != null){ + if(level < min){ + throw new BadRequestException("权限不足,你的角色级别:" + min + ",低于操作的角色级别:" + level); + } + } + return min; + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/UserBankController.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/UserBankController.java new file mode 100644 index 0000000..229f892 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/UserBankController.java @@ -0,0 +1,90 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.rest; + +import me.zhengjie.annotation.Log; +import me.zhengjie.modules.system.domain.UserBank; +import me.zhengjie.modules.system.service.UserBankService; +import me.zhengjie.modules.system.service.dto.UserBankQueryCriteria; +import org.springframework.data.domain.Pageable; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import io.swagger.annotations.*; +import java.io.IOException; +import javax.servlet.http.HttpServletResponse; +import me.zhengjie.utils.PageResult; +import me.zhengjie.modules.system.service.dto.UserBankDto; + +/** +* @website https://eladmin.vip +* @author author +* @date 2023-10-31 +**/ +@RestController +@RequiredArgsConstructor +@Api(tags = "银行卡信息管理") +@RequestMapping("/api/userBank") +public class UserBankController { + + private final UserBankService userBankService; + + @Log("导出数据") + @ApiOperation("导出数据") + @GetMapping(value = "/download") + @PreAuthorize("@el.check('userBank:list')") + public void exportUserBank(HttpServletResponse response, UserBankQueryCriteria criteria) throws IOException { + userBankService.download(userBankService.queryAll(criteria), response); + } + + @GetMapping + @Log("查询银行卡信息") + @ApiOperation("查询银行卡信息") + @PreAuthorize("@el.check('userBank:list')") + public ResponseEntity> queryUserBank(UserBankQueryCriteria criteria, Pageable pageable){ + return new ResponseEntity<>(userBankService.queryAll(criteria,pageable),HttpStatus.OK); + } + + @PostMapping + @Log("新增银行卡信息") + @ApiOperation("新增银行卡信息") + @PreAuthorize("@el.check('userBank:add')") + public ResponseEntity createUserBank(@Validated @RequestBody UserBank resources){ + userBankService.create(resources); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @PutMapping + @Log("修改银行卡信息") + @ApiOperation("修改银行卡信息") + @PreAuthorize("@el.check('userBank:edit')") + public ResponseEntity updateUserBank(@Validated @RequestBody UserBank resources){ + userBankService.update(resources); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @DeleteMapping + @Log("删除银行卡信息") + @ApiOperation("删除银行卡信息") + @PreAuthorize("@el.check('userBank:del')") + public ResponseEntity deleteUserBank(@RequestBody Long[] ids) { + userBankService.deleteAll(ids); + return new ResponseEntity<>(HttpStatus.OK); + } +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/UserController.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/UserController.java new file mode 100644 index 0000000..f75b8bb --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/UserController.java @@ -0,0 +1,210 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.rest; + +import cn.hutool.core.collection.CollectionUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import me.zhengjie.annotation.Log; +import me.zhengjie.utils.PageResult; +import me.zhengjie.config.RsaProperties; +import me.zhengjie.modules.system.domain.Dept; +import me.zhengjie.modules.system.service.DataService; +import me.zhengjie.modules.system.domain.User; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.modules.system.domain.vo.UserPassVo; +import me.zhengjie.modules.system.service.DeptService; +import me.zhengjie.modules.system.service.RoleService; +import me.zhengjie.modules.system.service.dto.RoleSmallDto; +import me.zhengjie.modules.system.service.dto.UserDto; +import me.zhengjie.modules.system.service.dto.UserQueryCriteria; +import me.zhengjie.modules.system.service.VerifyService; +import me.zhengjie.utils.*; +import me.zhengjie.modules.system.service.UserService; +import me.zhengjie.utils.enums.CodeEnum; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author Zheng Jie + * @date 2018-11-23 + */ +@Api(tags = "系统:用户管理") +@RestController +@RequestMapping("/api/users") +@RequiredArgsConstructor +public class UserController { + + private final PasswordEncoder passwordEncoder; + private final UserService userService; + private final DataService dataService; + private final DeptService deptService; + private final RoleService roleService; + private final VerifyService verificationCodeService; + + @ApiOperation("导出用户数据") + @GetMapping(value = "/download") + @PreAuthorize("@el.check('user:list')") + public void exportUser(HttpServletResponse response, UserQueryCriteria criteria) throws IOException { + userService.download(userService.queryAll(criteria), response); + } + + @ApiOperation("查询用户") + @GetMapping + @PreAuthorize("@el.check('user:list')") + public ResponseEntity> queryUser(UserQueryCriteria criteria, Pageable pageable){ + if (!ObjectUtils.isEmpty(criteria.getDeptId())) { + criteria.getDeptIds().add(criteria.getDeptId()); + // 先查找是否存在子节点 + List data = deptService.findByPid(criteria.getDeptId()); + // 然后把子节点的ID都加入到集合中 + criteria.getDeptIds().addAll(deptService.getDeptChildren(data)); + } + // 数据权限 + List dataScopes = dataService.getDeptIds(userService.findByName(SecurityUtils.getCurrentUsername())); + // criteria.getDeptIds() 不为空并且数据权限不为空则取交集 + if (!CollectionUtils.isEmpty(criteria.getDeptIds()) && !CollectionUtils.isEmpty(dataScopes)){ + // 取交集 + criteria.getDeptIds().retainAll(dataScopes); + if(!CollectionUtil.isEmpty(criteria.getDeptIds())){ + return new ResponseEntity<>(userService.queryAll(criteria,pageable),HttpStatus.OK); + } + } else { + // 否则取并集 + criteria.getDeptIds().addAll(dataScopes); + return new ResponseEntity<>(userService.queryAll(criteria,pageable),HttpStatus.OK); + } + return new ResponseEntity<>(PageUtil.noData(),HttpStatus.OK); + } + + @Log("新增用户") + @ApiOperation("新增用户") + @PostMapping + @PreAuthorize("@el.check('user:add')") + public ResponseEntity createUser(@Validated @RequestBody User resources){ + checkLevel(resources); + // 默认密码 123456 + resources.setPassword(passwordEncoder.encode("123456")); + userService.create(resources); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @Log("修改用户") + @ApiOperation("修改用户") + @PutMapping + @PreAuthorize("@el.check('user:edit')") + public ResponseEntity updateUser(@Validated(User.Update.class) @RequestBody User resources) throws Exception { + checkLevel(resources); + userService.update(resources); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @Log("修改用户:个人中心") + @ApiOperation("修改用户:个人中心") + @PutMapping(value = "center") + public ResponseEntity centerUser(@Validated(User.Update.class) @RequestBody User resources){ + if(!resources.getId().equals(SecurityUtils.getCurrentUserId())){ + throw new BadRequestException("不能修改他人资料"); + } + userService.updateCenter(resources); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @Log("删除用户") + @ApiOperation("删除用户") + @DeleteMapping + @PreAuthorize("@el.check('user:del')") + public ResponseEntity deleteUser(@RequestBody Set ids){ + for (Long id : ids) { + Integer currentLevel = Collections.min(roleService.findByUsersId(SecurityUtils.getCurrentUserId()).stream().map(RoleSmallDto::getLevel).collect(Collectors.toList())); + Integer optLevel = Collections.min(roleService.findByUsersId(id).stream().map(RoleSmallDto::getLevel).collect(Collectors.toList())); + if (currentLevel > optLevel) { + throw new BadRequestException("角色权限不足,不能删除:" + userService.findById(id).getUsername()); + } + } + userService.delete(ids); + return new ResponseEntity<>(HttpStatus.OK); + } + + @ApiOperation("修改密码") + @PostMapping(value = "/updatePass") + public ResponseEntity updateUserPass(@RequestBody UserPassVo passVo) throws Exception { + String oldPass = RsaUtils.decryptByPrivateKey(RsaProperties.privateKey,passVo.getOldPass()); + String newPass = RsaUtils.decryptByPrivateKey(RsaProperties.privateKey,passVo.getNewPass()); + UserDto user = userService.findByName(SecurityUtils.getCurrentUsername()); + if(!passwordEncoder.matches(oldPass, user.getPassword())){ + throw new BadRequestException("修改失败,旧密码错误"); + } + if(passwordEncoder.matches(newPass, user.getPassword())){ + throw new BadRequestException("新密码不能与旧密码相同"); + } + userService.updatePass(user.getUsername(),passwordEncoder.encode(newPass)); + return new ResponseEntity<>(HttpStatus.OK); + } + + @ApiOperation("重置密码") + @PutMapping(value = "/resetPwd") + public ResponseEntity resetPwd(@RequestBody Set ids) { + String pwd = passwordEncoder.encode("123456"); + userService.resetPwd(ids, pwd); + return new ResponseEntity<>(HttpStatus.OK); + } + + @ApiOperation("修改头像") + @PostMapping(value = "/updateAvatar") + public ResponseEntity updateUserAvatar(@RequestParam MultipartFile avatar){ + return new ResponseEntity<>(userService.updateAvatar(avatar), HttpStatus.OK); + } + + @Log("修改邮箱") + @ApiOperation("修改邮箱") + @PostMapping(value = "/updateEmail/{code}") + public ResponseEntity updateUserEmail(@PathVariable String code,@RequestBody User user) throws Exception { + String password = RsaUtils.decryptByPrivateKey(RsaProperties.privateKey,user.getPassword()); + UserDto userDto = userService.findByName(SecurityUtils.getCurrentUsername()); + if(!passwordEncoder.matches(password, userDto.getPassword())){ + throw new BadRequestException("密码错误"); + } + verificationCodeService.validated(CodeEnum.EMAIL_RESET_EMAIL_CODE.getKey() + user.getEmail(), code); + userService.updateEmail(userDto.getUsername(),user.getEmail()); + return new ResponseEntity<>(HttpStatus.OK); + } + + /** + * 如果当前用户的角色级别低于创建用户的角色级别,则抛出权限不足的错误 + * @param resources / + */ + private void checkLevel(User resources) { + Integer currentLevel = Collections.min(roleService.findByUsersId(SecurityUtils.getCurrentUserId()).stream().map(RoleSmallDto::getLevel).collect(Collectors.toList())); + Integer optLevel = roleService.findByRoles(resources.getRoles()); + if (currentLevel > optLevel) { + throw new BadRequestException("角色权限不足"); + } + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/VerifyController.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/VerifyController.java new file mode 100644 index 0000000..09ba44b --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/rest/VerifyController.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import me.zhengjie.domain.vo.EmailVo; +import me.zhengjie.service.EmailService; +import me.zhengjie.modules.system.service.VerifyService; +import me.zhengjie.utils.enums.CodeBiEnum; +import me.zhengjie.utils.enums.CodeEnum; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import java.util.Objects; + +/** + * @author Zheng Jie + * @date 2018-12-26 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/code") +@Api(tags = "系统:验证码管理") +public class VerifyController { + + private final VerifyService verificationCodeService; + private final EmailService emailService; + + @PostMapping(value = "/resetEmail") + @ApiOperation("重置邮箱,发送验证码") + public ResponseEntity resetEmail(@RequestParam String email){ + EmailVo emailVo = verificationCodeService.sendEmail(email, CodeEnum.EMAIL_RESET_EMAIL_CODE.getKey()); + emailService.send(emailVo,emailService.find()); + return new ResponseEntity<>(HttpStatus.OK); + } + + @PostMapping(value = "/email/resetPass") + @ApiOperation("重置密码,发送验证码") + public ResponseEntity resetPass(@RequestParam String email){ + EmailVo emailVo = verificationCodeService.sendEmail(email, CodeEnum.EMAIL_RESET_PWD_CODE.getKey()); + emailService.send(emailVo,emailService.find()); + return new ResponseEntity<>(HttpStatus.OK); + } + + @GetMapping(value = "/validated") + @ApiOperation("验证码验证") + public ResponseEntity validated(@RequestParam String email, @RequestParam String code, @RequestParam Integer codeBi){ + CodeBiEnum biEnum = CodeBiEnum.find(codeBi); + switch (Objects.requireNonNull(biEnum)){ + case ONE: + verificationCodeService.validated(CodeEnum.EMAIL_RESET_EMAIL_CODE.getKey() + email ,code); + break; + case TWO: + verificationCodeService.validated(CodeEnum.EMAIL_RESET_PWD_CODE.getKey() + email ,code); + break; + default: + break; + } + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/DataService.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/DataService.java new file mode 100644 index 0000000..10258d9 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/DataService.java @@ -0,0 +1,34 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service; + +import me.zhengjie.modules.system.service.dto.UserDto; +import java.util.List; + +/** + * 数据权限服务类 + * @author Zheng Jie + * @date 2020-05-07 + */ +public interface DataService { + + /** + * 获取数据权限 + * @param user / + * @return / + */ + List getDeptIds(UserDto user); +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/DeptService.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/DeptService.java new file mode 100644 index 0000000..902f603 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/DeptService.java @@ -0,0 +1,124 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service; + +import me.zhengjie.modules.system.domain.Dept; +import me.zhengjie.modules.system.service.dto.DeptDto; +import me.zhengjie.modules.system.service.dto.DeptQueryCriteria; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +/** +* @author Zheng Jie +* @date 2019-03-25 +*/ +public interface DeptService { + + /** + * 查询所有数据 + * @param criteria 条件 + * @param isQuery / + * @throws Exception / + * @return / + */ + List queryAll(DeptQueryCriteria criteria, Boolean isQuery) throws Exception; + + /** + * 根据ID查询 + * @param id / + * @return / + */ + DeptDto findById(Long id); + + /** + * 创建 + * @param resources / + */ + void create(Dept resources); + + /** + * 编辑 + * @param resources / + */ + void update(Dept resources); + + /** + * 删除 + * @param deptDtos / + * + */ + void delete(Set deptDtos); + + /** + * 根据PID查询 + * @param pid / + * @return / + */ + List findByPid(long pid); + + /** + * 根据角色ID查询 + * @param id / + * @return / + */ + Set findByRoleId(Long id); + + /** + * 导出数据 + * @param queryAll 待导出的数据 + * @param response / + * @throws IOException / + */ + void download(List queryAll, HttpServletResponse response) throws IOException; + + /** + * 获取待删除的部门 + * @param deptList / + * @param deptDtos / + * @return / + */ + Set getDeleteDepts(List deptList, Set deptDtos); + + /** + * 根据ID获取同级与上级数据 + * @param deptDto / + * @param depts / + * @return / + */ + List getSuperior(DeptDto deptDto, List depts); + + /** + * 构建树形数据 + * @param deptDtos / + * @return / + */ + Object buildTree(List deptDtos); + + /** + * 获取 + * @param deptList / + * @return / + */ + List getDeptChildren(List deptList); + + /** + * 验证是否被角色或用户关联 + * @param deptDtos / + */ + void verification(Set deptDtos); +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/DictDetailService.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/DictDetailService.java new file mode 100644 index 0000000..7e2efb7 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/DictDetailService.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service; + +import me.zhengjie.utils.PageResult; +import me.zhengjie.modules.system.domain.DictDetail; +import me.zhengjie.modules.system.service.dto.DictDetailDto; +import me.zhengjie.modules.system.service.dto.DictDetailQueryCriteria; +import org.springframework.data.domain.Pageable; +import java.util.List; + +/** +* @author Zheng Jie +* @date 2019-04-10 +*/ +public interface DictDetailService { + + /** + * 创建 + * @param resources / + */ + void create(DictDetail resources); + + /** + * 编辑 + * @param resources / + */ + void update(DictDetail resources); + + /** + * 删除 + * @param id / + */ + void delete(Long id); + + /** + * 分页查询 + * @param criteria 条件 + * @param pageable 分页参数 + * @return / + */ + PageResult queryAll(DictDetailQueryCriteria criteria, Pageable pageable); + + /** + * 根据字典名称获取字典详情 + * @param name 字典名称 + * @return / + */ + List getDictByName(String name); +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/DictService.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/DictService.java new file mode 100644 index 0000000..05077b7 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/DictService.java @@ -0,0 +1,75 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service; + +import me.zhengjie.utils.PageResult; +import me.zhengjie.modules.system.domain.Dict; +import me.zhengjie.modules.system.service.dto.DictDto; +import me.zhengjie.modules.system.service.dto.DictQueryCriteria; +import org.springframework.data.domain.Pageable; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +/** +* @author Zheng Jie +* @date 2019-04-10 +*/ +public interface DictService { + + /** + * 分页查询 + * @param criteria 条件 + * @param pageable 分页参数 + * @return / + */ + PageResult queryAll(DictQueryCriteria criteria, Pageable pageable); + + /** + * 查询全部数据 + * @param dict / + * @return / + */ + List queryAll(DictQueryCriteria dict); + + /** + * 创建 + * @param resources / + * @return / + */ + void create(Dict resources); + + /** + * 编辑 + * @param resources / + */ + void update(Dict resources); + + /** + * 删除 + * @param ids / + */ + void delete(Set ids); + + /** + * 导出数据 + * @param queryAll 待导出的数据 + * @param response / + * @throws IOException / + */ + void download(List queryAll, HttpServletResponse response) throws IOException; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/JobService.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/JobService.java new file mode 100644 index 0000000..dab9f58 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/JobService.java @@ -0,0 +1,88 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service; + +import me.zhengjie.utils.PageResult; +import me.zhengjie.modules.system.domain.Job; +import me.zhengjie.modules.system.service.dto.JobDto; +import me.zhengjie.modules.system.service.dto.JobQueryCriteria; +import org.springframework.data.domain.Pageable; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +/** +* @author Zheng Jie +* @date 2019-03-29 +*/ +public interface JobService { + + /** + * 根据ID查询 + * @param id / + * @return / + */ + JobDto findById(Long id); + + /** + * 创建 + * @param resources / + * @return / + */ + void create(Job resources); + + /** + * 编辑 + * @param resources / + */ + void update(Job resources); + + /** + * 删除 + * @param ids / + */ + void delete(Set ids); + + /** + * 分页查询 + * @param criteria 条件 + * @param pageable 分页参数 + * @return / + */ + PageResult queryAll(JobQueryCriteria criteria, Pageable pageable); + + /** + * 查询全部数据 + * @param criteria / + * @return / + */ + List queryAll(JobQueryCriteria criteria); + + /** + * 导出数据 + * @param queryAll 待导出的数据 + * @param response / + * @throws IOException / + */ + void download(List queryAll, HttpServletResponse response) throws IOException; + + /** + * 验证是否被用户关联 + * @param ids / + */ + void verification(Set ids); +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/MenuService.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/MenuService.java new file mode 100644 index 0000000..1116d10 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/MenuService.java @@ -0,0 +1,126 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service; + +import me.zhengjie.modules.system.domain.Menu; +import me.zhengjie.modules.system.domain.vo.MenuVo; +import me.zhengjie.modules.system.service.dto.MenuDto; +import me.zhengjie.modules.system.service.dto.MenuQueryCriteria; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +/** + * @author Zheng Jie + * @date 2018-12-17 + */ +public interface MenuService { + + /** + * 查询全部数据 + * @param criteria 条件 + * @param isQuery / + * @throws Exception / + * @return / + */ + List queryAll(MenuQueryCriteria criteria, Boolean isQuery) throws Exception; + + /** + * 根据ID查询 + * @param id / + * @return / + */ + MenuDto findById(long id); + + /** + * 创建 + * @param resources / + */ + void create(Menu resources); + + /** + * 编辑 + * @param resources / + */ + void update(Menu resources); + + /** + * 获取所有子节点,包含自身ID + * @param menuList / + * @param menuSet / + * @return / + */ + Set getChildMenus(List menuList, Set menuSet); + + /** + * 构建菜单树 + * @param menuDtos 原始数据 + * @return / + */ + List buildTree(List menuDtos); + + /** + * 构建菜单树 + * @param menuDtos / + * @return / + */ + List buildMenus(List menuDtos); + + /** + * 根据ID查询 + * @param id / + * @return / + */ + Menu findOne(Long id); + + /** + * 删除 + * @param menuSet / + */ + void delete(Set menuSet); + + /** + * 导出 + * @param queryAll 待导出的数据 + * @param response / + * @throws IOException / + */ + void download(List queryAll, HttpServletResponse response) throws IOException; + + /** + * 懒加载菜单数据 + * @param pid / + * @return / + */ + List getMenus(Long pid); + + /** + * 根据ID获取同级与上级数据 + * @param menuDto / + * @param objects / + * @return / + */ + List getSuperior(MenuDto menuDto, List objects); + + /** + * 根据当前用户获取菜单 + * @param currentUserId / + * @return / + */ + List findByUser(Long currentUserId); +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/MonitorService.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/MonitorService.java new file mode 100644 index 0000000..478225a --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/MonitorService.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service; + +import java.util.Map; + +/** + * @author Zheng Jie + * @date 2020-05-02 + */ +public interface MonitorService { + + /** + * 查询数据分页 + * @return Map + */ + Map getServers(); +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/PurchaseService.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/PurchaseService.java new file mode 100644 index 0000000..64dfd30 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/PurchaseService.java @@ -0,0 +1,83 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.service; + +import me.zhengjie.modules.system.domain.Purchase; +import me.zhengjie.modules.system.service.dto.PurchaseDto; +import me.zhengjie.modules.system.service.dto.PurchaseQueryCriteria; +import org.springframework.data.domain.Pageable; +import java.util.Map; +import java.util.List; +import java.io.IOException; +import javax.servlet.http.HttpServletResponse; +import me.zhengjie.utils.PageResult; + +/** +* @website https://eladmin.vip +* @description 服务接口 +* @author author +* +**/ +public interface PurchaseService { + + /** + * 查询数据分页 + * @param criteria 条件 + * @param pageable 分页参数 + * @return Map + */ + PageResult queryAll(PurchaseQueryCriteria criteria, Pageable pageable); + + /** + * 查询所有数据不分页 + * @param criteria 条件参数 + * @return List + */ + List queryAll(PurchaseQueryCriteria criteria); + + /** + * 根据ID查询 + * @param purchaseId ID + * @return PurchaseDto + */ + PurchaseDto findById(Long purchaseId); + + /** + * 创建 + * @param resources / + */ + void create(Purchase resources); + + /** + * 编辑 + * @param resources / + */ + void update(Purchase resources); + + /** + * 多选删除 + * @param ids / + */ + void deleteAll(Long[] ids); + + /** + * 导出数据 + * @param all 待导出的数据 + * @param response / + * @throws IOException / + */ + void download(List all, HttpServletResponse response) throws IOException; +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/ReimburseService.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/ReimburseService.java new file mode 100644 index 0000000..fe7a48e --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/ReimburseService.java @@ -0,0 +1,83 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.service; + +import me.zhengjie.modules.system.domain.Reimburse; +import me.zhengjie.modules.system.service.dto.ReimburseDto; +import me.zhengjie.modules.system.service.dto.ReimburseQueryCriteria; +import org.springframework.data.domain.Pageable; +import java.util.Map; +import java.util.List; +import java.io.IOException; +import javax.servlet.http.HttpServletResponse; +import me.zhengjie.utils.PageResult; + +/** +* @website https://eladmin.vip +* @description 服务接口 +* @author author +* +**/ +public interface ReimburseService { + + /** + * 查询数据分页 + * @param criteria 条件 + * @param pageable 分页参数 + * @return Map + */ + PageResult queryAll(ReimburseQueryCriteria criteria, Pageable pageable); + + /** + * 查询所有数据不分页 + * @param criteria 条件参数 + * @return List + */ + List queryAll(ReimburseQueryCriteria criteria); + + /** + * 根据ID查询 + * @param reimburseId ID + * @return ReimburseDto + */ + ReimburseDto findById(Long reimburseId); + + /** + * 创建 + * @param resources / + */ + void create(Reimburse resources); + + /** + * 编辑 + * @param resources / + */ + void update(Reimburse resources); + + /** + * 多选删除 + * @param ids / + */ + void deleteAll(Long[] ids); + + /** + * 导出数据 + * @param all 待导出的数据 + * @param response / + * @throws IOException / + */ + void download(List all, HttpServletResponse response) throws IOException; +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/RoleService.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/RoleService.java new file mode 100644 index 0000000..6bc8cce --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/RoleService.java @@ -0,0 +1,137 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service; + +import me.zhengjie.modules.security.service.dto.AuthorityDto; +import me.zhengjie.modules.system.domain.Role; +import me.zhengjie.modules.system.service.dto.RoleDto; +import me.zhengjie.modules.system.service.dto.RoleQueryCriteria; +import me.zhengjie.modules.system.service.dto.RoleSmallDto; +import me.zhengjie.modules.system.service.dto.UserDto; +import me.zhengjie.utils.PageResult; +import org.springframework.data.domain.Pageable; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +/** + * @author Zheng Jie + * @date 2018-12-03 + */ +public interface RoleService { + + /** + * 查询全部数据 + * @return / + */ + List queryAll(); + + /** + * 根据ID查询 + * @param id / + * @return / + */ + RoleDto findById(long id); + + /** + * 创建 + * @param resources / + */ + void create(Role resources); + + /** + * 编辑 + * @param resources / + */ + void update(Role resources); + + /** + * 删除 + * @param ids / + */ + void delete(Set ids); + + /** + * 根据用户ID查询 + * @param id 用户ID + * @return / + */ + List findByUsersId(Long id); + + /** + * 根据角色查询角色级别 + * @param roles / + * @return / + */ + Integer findByRoles(Set roles); + + /** + * 修改绑定的菜单 + * @param resources / + * @param roleDTO / + */ + void updateMenu(Role resources, RoleDto roleDTO); + + /** + * 解绑菜单 + * @param id / + */ + void untiedMenu(Long id); + + /** + * 待条件分页查询 + * @param criteria 条件 + * @param pageable 分页参数 + * @return / + */ + PageResult queryAll(RoleQueryCriteria criteria, Pageable pageable); + + /** + * 查询全部 + * @param criteria 条件 + * @return / + */ + List queryAll(RoleQueryCriteria criteria); + + /** + * 导出数据 + * @param queryAll 待导出的数据 + * @param response / + * @throws IOException / + */ + void download(List queryAll, HttpServletResponse response) throws IOException; + + /** + * 获取用户权限信息 + * @param user 用户信息 + * @return 权限信息 + */ + List mapToGrantedAuthorities(UserDto user); + + /** + * 验证是否被用户关联 + * @param ids / + */ + void verification(Set ids); + + /** + * 根据菜单Id查询 + * @param menuIds / + * @return / + */ + List findInMenuId(List menuIds); +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/UserBankService.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/UserBankService.java new file mode 100644 index 0000000..560d55a --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/UserBankService.java @@ -0,0 +1,83 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.service; + +import me.zhengjie.modules.system.domain.UserBank; +import me.zhengjie.modules.system.service.dto.UserBankDto; +import me.zhengjie.modules.system.service.dto.UserBankQueryCriteria; +import org.springframework.data.domain.Pageable; +import java.util.Map; +import java.util.List; +import java.io.IOException; +import javax.servlet.http.HttpServletResponse; +import me.zhengjie.utils.PageResult; + +/** +* @website https://eladmin.vip +* @description 服务接口 +* @author author +* @date 2023-10-31 +**/ +public interface UserBankService { + + /** + * 查询数据分页 + * @param criteria 条件 + * @param pageable 分页参数 + * @return Map + */ + PageResult queryAll(UserBankQueryCriteria criteria, Pageable pageable); + + /** + * 查询所有数据不分页 + * @param criteria 条件参数 + * @return List + */ + List queryAll(UserBankQueryCriteria criteria); + + /** + * 根据ID查询 + * @param bankId ID + * @return UserBankDto + */ + UserBankDto findById(Long bankId); + + /** + * 创建 + * @param resources / + */ + void create(UserBank resources); + + /** + * 编辑 + * @param resources / + */ + void update(UserBank resources); + + /** + * 多选删除 + * @param ids / + */ + void deleteAll(Long[] ids); + + /** + * 导出数据 + * @param all 待导出的数据 + * @param response / + * @throws IOException / + */ + void download(List all, HttpServletResponse response) throws IOException; +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/UserService.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/UserService.java new file mode 100644 index 0000000..a3cc4f2 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/UserService.java @@ -0,0 +1,133 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service; + +import me.zhengjie.utils.PageResult; +import me.zhengjie.modules.system.domain.User; +import me.zhengjie.modules.system.service.dto.UserDto; +import me.zhengjie.modules.system.service.dto.UserLoginDto; +import me.zhengjie.modules.system.service.dto.UserQueryCriteria; +import org.springframework.data.domain.Pageable; +import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author Zheng Jie + * @date 2018-11-23 + */ +public interface UserService { + + /** + * 根据ID查询 + * @param id ID + * @return / + */ + UserDto findById(long id); + + /** + * 新增用户 + * @param resources / + */ + void create(User resources); + + /** + * 编辑用户 + * @param resources / + * @throws Exception / + */ + void update(User resources) throws Exception; + + /** + * 删除用户 + * @param ids / + */ + void delete(Set ids); + + /** + * 根据用户名查询 + * @param userName / + * @return / + */ + UserDto findByName(String userName); + + /** + * 根据用户名查询 + * @param userName / + * @return / + */ + UserLoginDto getLoginData(String userName); + + /** + * 修改密码 + * @param username 用户名 + * @param encryptPassword 密码 + */ + void updatePass(String username, String encryptPassword); + + /** + * 修改头像 + * @param file 文件 + * @return / + */ + Map updateAvatar(MultipartFile file); + + /** + * 修改邮箱 + * @param username 用户名 + * @param email 邮箱 + */ + void updateEmail(String username, String email); + + /** + * 查询全部 + * @param criteria 条件 + * @param pageable 分页参数 + * @return / + */ + PageResult queryAll(UserQueryCriteria criteria, Pageable pageable); + + /** + * 查询全部不分页 + * @param criteria 条件 + * @return / + */ + List queryAll(UserQueryCriteria criteria); + + /** + * 导出数据 + * @param queryAll 待导出的数据 + * @param response / + * @throws IOException / + */ + void download(List queryAll, HttpServletResponse response) throws IOException; + + /** + * 用户自助修改资料 + * @param resources / + */ + void updateCenter(User resources); + + /** + * 重置密码 + * @param ids 用户id + * @param pwd 密码 + */ + void resetPwd(Set ids, String pwd); +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/VerifyService.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/VerifyService.java new file mode 100644 index 0000000..4ca39b9 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/VerifyService.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service; + +import me.zhengjie.domain.vo.EmailVo; + +/** + * @author Zheng Jie + * @date 2018-12-26 + */ +public interface VerifyService { + + /** + * 发送验证码 + * @param email / + * @param key / + * @return / + */ + EmailVo sendEmail(String email, String key); + + + /** + * 验证 + * @param code / + * @param key / + */ + void validated(String key, String code); +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DeptDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DeptDto.java new file mode 100644 index 0000000..bc236a1 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DeptDto.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.dto; + +import lombok.Getter; +import lombok.Setter; +import me.zhengjie.base.BaseDTO; +import java.io.Serializable; +import java.util.List; +import java.util.Objects; + +/** +* @author Zheng Jie +* @date 2019-03-25 +*/ +@Getter +@Setter +public class DeptDto extends BaseDTO implements Serializable { + + private Long id; + + private String name; + + private Boolean enabled; + + private Integer deptSort; + + private List children; + + private Long pid; + + private Integer subCount; + + public Boolean getHasChildren() { + return subCount > 0; + } + + public Boolean getLeaf() { + return subCount <= 0; + } + + public String getLabel() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DeptDto deptDto = (DeptDto) o; + return Objects.equals(id, deptDto.id) && + Objects.equals(name, deptDto.name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name); + } +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DeptQueryCriteria.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DeptQueryCriteria.java new file mode 100644 index 0000000..4d8cdf2 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DeptQueryCriteria.java @@ -0,0 +1,46 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.dto; + +import lombok.Data; +import me.zhengjie.annotation.DataPermission; +import me.zhengjie.annotation.Query; +import java.sql.Timestamp; +import java.util.List; + +/** +* @author Zheng Jie +* @date 2019-03-25 +*/ +@Data +@DataPermission(fieldName = "id") +public class DeptQueryCriteria{ + + @Query(type = Query.Type.INNER_LIKE) + private String name; + + @Query + private Boolean enabled; + + @Query + private Long pid; + + @Query(type = Query.Type.IS_NULL, propName = "pid") + private Boolean pidIsNull; + + @Query(type = Query.Type.BETWEEN) + private List createTime; +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DeptSmallDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DeptSmallDto.java new file mode 100644 index 0000000..4dc64e5 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DeptSmallDto.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.dto; + +import lombok.Data; +import java.io.Serializable; + +/** +* @author Zheng Jie +* @date 2019-6-10 16:32:18 +*/ +@Data +public class DeptSmallDto implements Serializable { + + private Long id; + + private String name; +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DictDetailDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DictDetailDto.java new file mode 100644 index 0000000..a4b931b --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DictDetailDto.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.dto; + +import lombok.Getter; +import lombok.Setter; +import me.zhengjie.base.BaseDTO; +import java.io.Serializable; + +/** +* @author Zheng Jie +* @date 2019-04-10 +*/ +@Getter +@Setter +public class DictDetailDto extends BaseDTO implements Serializable { + + private Long id; + + private DictSmallDto dict; + + private String label; + + private String value; + + private Integer dictSort; +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DictDetailQueryCriteria.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DictDetailQueryCriteria.java new file mode 100644 index 0000000..91a01b1 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DictDetailQueryCriteria.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.dto; + +import lombok.Data; +import me.zhengjie.annotation.Query; + +/** +* @author Zheng Jie +* @date 2019-04-10 +*/ +@Data +public class DictDetailQueryCriteria { + + @Query(type = Query.Type.INNER_LIKE) + private String label; + + @Query(propName = "name",joinName = "dict") + private String dictName; +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DictDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DictDto.java new file mode 100644 index 0000000..048b4a1 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DictDto.java @@ -0,0 +1,39 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.dto; + +import lombok.Getter; +import lombok.Setter; +import me.zhengjie.base.BaseDTO; +import java.io.Serializable; +import java.util.List; + +/** +* @author Zheng Jie +* @date 2019-04-10 +*/ +@Getter +@Setter +public class DictDto extends BaseDTO implements Serializable { + + private Long id; + + private List dictDetails; + + private String name; + + private String description; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DictQueryCriteria.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DictQueryCriteria.java new file mode 100644 index 0000000..7207c61 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DictQueryCriteria.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.dto; + +import lombok.Data; +import me.zhengjie.annotation.Query; + +/** + * @author Zheng Jie + * 公共查询类 + */ +@Data +public class DictQueryCriteria { + + @Query(blurry = "name,description") + private String blurry; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DictSmallDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DictSmallDto.java new file mode 100644 index 0000000..98fc6ee --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/DictSmallDto.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.dto; + +import lombok.Getter; +import lombok.Setter; +import java.io.Serializable; + +/** +* @author Zheng Jie +* @date 2019-04-10 +*/ +@Getter +@Setter +public class DictSmallDto implements Serializable { + + private Long id; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/JobDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/JobDto.java new file mode 100644 index 0000000..8836d55 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/JobDto.java @@ -0,0 +1,46 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import me.zhengjie.base.BaseDTO; + +import java.io.Serializable; + +/** +* @author Zheng Jie +* @date 2019-03-29 +*/ +@Getter +@Setter +@NoArgsConstructor +public class JobDto extends BaseDTO implements Serializable { + + private Long id; + + private Integer jobSort; + + private String name; + + private Boolean enabled; + + public JobDto(String name, Boolean enabled) { + this.name = name; + this.enabled = enabled; + } +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/JobQueryCriteria.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/JobQueryCriteria.java new file mode 100644 index 0000000..ee851dc --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/JobQueryCriteria.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; +import me.zhengjie.annotation.Query; +import java.sql.Timestamp; +import java.util.List; + +/** +* @author Zheng Jie +* @date 2019-6-4 14:49:34 +*/ +@Data +@NoArgsConstructor +public class JobQueryCriteria { + + @Query(type = Query.Type.INNER_LIKE) + private String name; + + @Query + private Boolean enabled; + + @Query(type = Query.Type.BETWEEN) + private List createTime; +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/JobSmallDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/JobSmallDto.java new file mode 100644 index 0000000..09cfa62 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/JobSmallDto.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; +import java.io.Serializable; + +/** +* @author Zheng Jie +* @date 2019-6-10 16:32:18 +*/ +@Data +@NoArgsConstructor +public class JobSmallDto implements Serializable { + + private Long id; + + private String name; +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/MenuDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/MenuDto.java new file mode 100644 index 0000000..d60dd29 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/MenuDto.java @@ -0,0 +1,91 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.dto; + +import lombok.Getter; +import lombok.Setter; +import me.zhengjie.base.BaseDTO; +import java.io.Serializable; +import java.util.List; +import java.util.Objects; + +/** + * @author Zheng Jie + * @date 2018-12-17 + */ +@Getter +@Setter +public class MenuDto extends BaseDTO implements Serializable { + + private Long id; + + private List children; + + private Integer type; + + private String permission; + + private String title; + + private Integer menuSort; + + private String path; + + private String component; + + private Long pid; + + private Integer subCount; + + private Boolean iFrame; + + private Boolean cache; + + private Boolean hidden; + + private String componentName; + + private String icon; + + public Boolean getHasChildren() { + return subCount > 0; + } + + public Boolean getLeaf() { + return subCount <= 0; + } + + public String getLabel() { + return title; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MenuDto menuDto = (MenuDto) o; + return Objects.equals(id, menuDto.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/MenuQueryCriteria.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/MenuQueryCriteria.java new file mode 100644 index 0000000..bd43e8d --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/MenuQueryCriteria.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.dto; + +import lombok.Data; +import me.zhengjie.annotation.Query; +import java.sql.Timestamp; +import java.util.List; + +/** + * @author Zheng Jie + * 公共查询类 + */ +@Data +public class MenuQueryCriteria { + + @Query(blurry = "title,component,permission") + private String blurry; + + @Query(type = Query.Type.BETWEEN) + private List createTime; + + @Query(type = Query.Type.IS_NULL, propName = "pid") + private Boolean pidIsNull; + + @Query + private Long pid; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/PurchaseDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/PurchaseDto.java new file mode 100644 index 0000000..6910cfb --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/PurchaseDto.java @@ -0,0 +1,93 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.service.dto; + +import lombok.Data; +import me.zhengjie.base.BaseDTO; + +import java.sql.Timestamp; +import java.math.BigDecimal; +import java.io.Serializable; + +/** +* @website https://eladmin.vip +* @description / +* @author author +* +**/ +@Data +public class PurchaseDto implements Serializable { + + /** ID */ + private Long purchaseId; + + /** 申请人 */ + private UserDto user; + + /** 申请事由 */ + private String reason; + + /** 申请金额 */ + private String fee; + + /** 货物/服务名称 */ + private String productName; + + /** 申请类型 */ + private String purchaseType; + + /** 品牌/规格/参数 */ + private String productInfo; + + /** 数量 */ + private BigDecimal productCount; + + /** 预算单价 */ + private BigDecimal productPrice; + + /** 预算总价 */ + private BigDecimal totalFee; + + /** 资金来源 */ + private String feeSource; + + /** 项目名称 */ + private String projectName; + + /** 项目编号 */ + private String projectNo; + + /** 预计采购时间 */ + private Timestamp purchaseTime; + + /** 申请时间 */ + private Timestamp applyTime; + + /** 备注 */ + private String remark; + + /** 创建者 */ + private String createBy; + + /** 更新者 */ + private String updateBy; + + /** 创建日期 */ + private Timestamp createTime; + + /** 更新时间 */ + private Timestamp updateTime; +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/PurchaseQueryCriteria.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/PurchaseQueryCriteria.java new file mode 100644 index 0000000..6f9c252 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/PurchaseQueryCriteria.java @@ -0,0 +1,33 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.service.dto; + +import lombok.Data; +import java.util.List; +import me.zhengjie.annotation.Query; + +/** +* @website https://eladmin.vip +* @author author +* +**/ +@Data +public class PurchaseQueryCriteria{ + + /** 精确 */ + @Query + private String purchaseType; +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/ReimburseDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/ReimburseDto.java new file mode 100644 index 0000000..b7320a8 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/ReimburseDto.java @@ -0,0 +1,81 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.service.dto; + +import lombok.Data; +import java.sql.Timestamp; +import java.io.Serializable; +import com.alibaba.fastjson.annotation.JSONField; +import com.alibaba.fastjson.serializer.ToStringSerializer; + +/** +* @website https://eladmin.vip +* @description / +* @author author +* +**/ +@Data +public class ReimburseDto implements Serializable { + + private Long reimburseId; + + /** 申请人 */ + private UserDto user; + + /** 申请事由 */ + private String reason; + + /** 申请金额 */ + private String fee; + + /** 报销类型 */ + private String reimburseType; + + /** 资金来源 */ + private String feeSource; + + /** 项目名称 */ + private String projectName; + + /** 项目编号 */ + private String projectNo; + + /** 项目预算 */ + private String projectFee; + + /** 花费产生时间 */ + private Timestamp reimburseTime; + + /** 申请时间 */ + private Timestamp applyTime; + + /** 备注 */ + private String remark; + + /** 创建者 */ + private String createBy; + + /** 更新者 */ + private String updateBy; + + /** 创建日期 */ + private Timestamp createTime; + + /** 更新时间 */ + private Timestamp updateTime; + + private String businessFeeType; +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/ReimburseQueryCriteria.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/ReimburseQueryCriteria.java new file mode 100644 index 0000000..4ad20af --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/ReimburseQueryCriteria.java @@ -0,0 +1,33 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.service.dto; + +import lombok.Data; +import java.util.List; +import me.zhengjie.annotation.Query; + +/** +* @website https://eladmin.vip +* @author author +* +**/ +@Data +public class ReimburseQueryCriteria{ + + /** 精确 */ + @Query + private String reimburseType; +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/RoleDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/RoleDto.java new file mode 100644 index 0000000..fc74d84 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/RoleDto.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.dto; + +import lombok.Getter; +import lombok.Setter; +import me.zhengjie.base.BaseDTO; +import java.io.Serializable; +import java.util.Objects; +import java.util.Set; + +/** + * @author Zheng Jie + * @date 2018-11-23 + */ +@Getter +@Setter +public class RoleDto extends BaseDTO implements Serializable { + + private Long id; + + private Set menus; + + private Set depts; + + private String name; + + private String dataScope; + + private Integer level; + + private String description; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RoleDto roleDto = (RoleDto) o; + return Objects.equals(id, roleDto.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/RoleQueryCriteria.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/RoleQueryCriteria.java new file mode 100644 index 0000000..5454fc4 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/RoleQueryCriteria.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.dto; + +import lombok.Data; +import me.zhengjie.annotation.Query; + +import java.sql.Timestamp; +import java.util.List; + +/** + * @author Zheng Jie + * 公共查询类 + */ +@Data +public class RoleQueryCriteria { + + @Query(blurry = "name,description") + private String blurry; + + @Query(type = Query.Type.BETWEEN) + private List createTime; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/RoleSmallDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/RoleSmallDto.java new file mode 100644 index 0000000..99215e6 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/RoleSmallDto.java @@ -0,0 +1,35 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.dto; + +import lombok.Data; +import java.io.Serializable; + +/** + * @author Zheng Jie + * @date 2018-11-23 + */ +@Data +public class RoleSmallDto implements Serializable { + + private Long id; + + private String name; + + private Integer level; + + private String dataScope; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/UserBankDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/UserBankDto.java new file mode 100644 index 0000000..ec0d208 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/UserBankDto.java @@ -0,0 +1,61 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.service.dto; + +import lombok.Data; +import java.sql.Timestamp; +import java.io.Serializable; + +/** +* @website https://eladmin.vip +* @description / +* @author author +* @date 2023-10-31 +**/ +@Data +public class UserBankDto implements Serializable { + + /** ID */ + private Long bankId; + + /** 所属用户 */ + private Long userId; + + /** 银行卡号 */ + private String bankNo; + + /** 银行卡所属支行 */ + private String bankName; + + /** 是否常用账号 */ + private String bankUse; + + /** 备注 */ + private String remark; + + /** 创建者 */ + private String createBy; + + /** 更新者 */ + private String updateBy; + + /** 创建日期 */ + private Timestamp createTime; + + /** 更新时间 */ + private Timestamp updateTime; + private UserDto user; +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/UserBankQueryCriteria.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/UserBankQueryCriteria.java new file mode 100644 index 0000000..410648f --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/UserBankQueryCriteria.java @@ -0,0 +1,29 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.service.dto; + +import lombok.Data; +import java.util.List; +import me.zhengjie.annotation.Query; + +/** +* @website https://eladmin.vip +* @author author +* @date 2023-10-31 +**/ +@Data +public class UserBankQueryCriteria{ +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/UserDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/UserDto.java new file mode 100644 index 0000000..d8a0173 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/UserDto.java @@ -0,0 +1,68 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.dto; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Getter; +import lombok.Setter; +import me.zhengjie.base.BaseDTO; +import java.io.Serializable; +import java.util.Date; +import java.util.Set; + +/** + * @author Zheng Jie + * @date 2018-11-23 + */ +@Getter +@Setter +public class UserDto extends BaseDTO implements Serializable { + + private Long id; + + private Set roles; + + private Set jobs; + + private DeptSmallDto dept; + + private Long deptId; + + private String username; + + private String nickName; + + private String email; + + private String phone; + + private String gender; + + private String avatarName; + + private String avatarPath; + + @JSONField(serialize = false) + private String password; + + private Boolean enabled; + + @JSONField(serialize = false) + private Boolean isAdmin = false; + + private Date pwdResetTime; + private String idNo; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/UserLoginDto.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/UserLoginDto.java new file mode 100644 index 0000000..13a51f7 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/UserLoginDto.java @@ -0,0 +1,28 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.dto; + +/** + * @author Zheng Jie + * @description 用户缓存时使用 + * @date 2022-05-26 + **/ +public class UserLoginDto extends UserDto { + + private String password; + + private Boolean isAdmin; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/UserQueryCriteria.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/UserQueryCriteria.java new file mode 100644 index 0000000..ad8e775 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/dto/UserQueryCriteria.java @@ -0,0 +1,49 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.dto; + +import lombok.Data; +import me.zhengjie.annotation.Query; +import java.io.Serializable; +import java.sql.Timestamp; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * @author Zheng Jie + * @date 2018-11-23 + */ +@Data +public class UserQueryCriteria implements Serializable { + + @Query + private Long id; + + @Query(propName = "id", type = Query.Type.IN, joinName = "dept") + private Set deptIds = new HashSet<>(); + + @Query(blurry = "email,username,nickName") + private String blurry; + + @Query + private Boolean enabled; + + private Long deptId; + + @Query(type = Query.Type.BETWEEN) + private List createTime; +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/DataServiceImpl.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/DataServiceImpl.java new file mode 100644 index 0000000..782fbd8 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/DataServiceImpl.java @@ -0,0 +1,91 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.impl; + +import lombok.RequiredArgsConstructor; +import me.zhengjie.modules.system.domain.Dept; +import me.zhengjie.modules.system.service.DataService; +import me.zhengjie.modules.system.service.DeptService; +import me.zhengjie.modules.system.service.RoleService; +import me.zhengjie.modules.system.service.dto.RoleSmallDto; +import me.zhengjie.modules.system.service.dto.UserDto; +import me.zhengjie.utils.enums.DataScopeEnum; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import java.util.*; + +/** + * @author Zheng Jie + * @website https://eladmin.vip + * @description 数据权限服务实现 + * @date 2020-05-07 + **/ +@Service +@RequiredArgsConstructor +@CacheConfig(cacheNames = "data") +public class DataServiceImpl implements DataService { + + private final RoleService roleService; + private final DeptService deptService; + + /** + * 用户角色和用户部门改变时需清理缓存 + * @param user / + * @return / + */ + @Override + @Cacheable(key = "'user:' + #p0.id") + public List getDeptIds(UserDto user) { + // 用于存储部门id + Set deptIds = new HashSet<>(); + // 查询用户角色 + List roleSet = roleService.findByUsersId(user.getId()); + // 获取对应的部门ID + for (RoleSmallDto role : roleSet) { + DataScopeEnum dataScopeEnum = DataScopeEnum.find(role.getDataScope()); + switch (Objects.requireNonNull(dataScopeEnum)) { + case THIS_LEVEL: + deptIds.add(user.getDept().getId()); + break; + case CUSTOMIZE: + deptIds.addAll(getCustomize(deptIds, role)); + break; + default: + return new ArrayList<>(); + } + } + return new ArrayList<>(deptIds); + } + + /** + * 获取自定义的数据权限 + * @param deptIds 部门ID + * @param role 角色 + * @return 数据权限ID + */ + public Set getCustomize(Set deptIds, RoleSmallDto role){ + Set depts = deptService.findByRoleId(role.getId()); + for (Dept dept : depts) { + deptIds.add(dept.getId()); + List deptChildren = deptService.findByPid(dept.getId()); + if (deptChildren != null && deptChildren.size() != 0) { + deptIds.addAll(deptService.getDeptChildren(deptChildren)); + } + } + return deptIds; + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/DeptServiceImpl.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/DeptServiceImpl.java new file mode 100644 index 0000000..a02bd69 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/DeptServiceImpl.java @@ -0,0 +1,283 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import lombok.RequiredArgsConstructor; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.modules.system.domain.Dept; +import me.zhengjie.modules.system.domain.User; +import me.zhengjie.modules.system.repository.RoleRepository; +import me.zhengjie.modules.system.repository.UserRepository; +import me.zhengjie.modules.system.service.dto.DeptDto; +import me.zhengjie.modules.system.service.dto.DeptQueryCriteria; +import me.zhengjie.utils.*; +import me.zhengjie.modules.system.repository.DeptRepository; +import me.zhengjie.modules.system.service.DeptService; +import me.zhengjie.modules.system.service.mapstruct.DeptMapper; +import me.zhengjie.utils.enums.DataScopeEnum; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.*; +import java.util.stream.Collectors; + +/** +* @author Zheng Jie +* @date 2019-03-25 +*/ +@Service +@RequiredArgsConstructor +@CacheConfig(cacheNames = "dept") +public class DeptServiceImpl implements DeptService { + + private final DeptRepository deptRepository; + private final DeptMapper deptMapper; + private final UserRepository userRepository; + private final RedisUtils redisUtils; + private final RoleRepository roleRepository; + + @Override + public List queryAll(DeptQueryCriteria criteria, Boolean isQuery) throws Exception { + Sort sort = Sort.by(Sort.Direction.ASC, "deptSort"); + String dataScopeType = SecurityUtils.getDataScopeType(); + if (isQuery) { + if(dataScopeType.equals(DataScopeEnum.ALL.getValue())){ + criteria.setPidIsNull(true); + } + List fields = QueryHelp.getAllFields(criteria.getClass(), new ArrayList<>()); + List fieldNames = new ArrayList(){{ add("pidIsNull");add("enabled");}}; + for (Field field : fields) { + //设置对象的访问权限,保证对private的属性的访问 + field.setAccessible(true); + Object val = field.get(criteria); + if(fieldNames.contains(field.getName())){ + continue; + } + if (ObjectUtil.isNotNull(val)) { + criteria.setPidIsNull(null); + break; + } + } + } + List list = deptMapper.toDto(deptRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder),sort)); + // 如果为空,就代表为自定义权限或者本级权限,就需要去重,不理解可以注释掉,看查询结果 + if(StringUtils.isBlank(dataScopeType)){ + return deduplication(list); + } + return list; + } + + @Override + @Cacheable(key = "'id:' + #p0") + public DeptDto findById(Long id) { + Dept dept = deptRepository.findById(id).orElseGet(Dept::new); + ValidationUtil.isNull(dept.getId(),"Dept","id",id); + return deptMapper.toDto(dept); + } + + @Override + public List findByPid(long pid) { + return deptRepository.findByPid(pid); + } + + @Override + public Set findByRoleId(Long id) { + return deptRepository.findByRoleId(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void create(Dept resources) { + deptRepository.save(resources); + // 计算子节点数目 + resources.setSubCount(0); + // 清理缓存 + updateSubCnt(resources.getPid()); + // 清理自定义角色权限的datascope缓存 + delCaches(resources.getPid()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(Dept resources) { + // 旧的部门 + Long oldPid = findById(resources.getId()).getPid(); + Long newPid = resources.getPid(); + if(resources.getPid() != null && resources.getId().equals(resources.getPid())) { + throw new BadRequestException("上级不能为自己"); + } + Dept dept = deptRepository.findById(resources.getId()).orElseGet(Dept::new); + ValidationUtil.isNull( dept.getId(),"Dept","id",resources.getId()); + resources.setId(dept.getId()); + deptRepository.save(resources); + // 更新父节点中子节点数目 + updateSubCnt(oldPid); + updateSubCnt(newPid); + // 清理缓存 + delCaches(resources.getId()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Set deptDtos) { + for (DeptDto deptDto : deptDtos) { + // 清理缓存 + delCaches(deptDto.getId()); + deptRepository.deleteById(deptDto.getId()); + updateSubCnt(deptDto.getPid()); + } + } + + @Override + public void download(List deptDtos, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (DeptDto deptDTO : deptDtos) { + Map map = new LinkedHashMap<>(); + map.put("部门名称", deptDTO.getName()); + map.put("部门状态", deptDTO.getEnabled() ? "启用" : "停用"); + map.put("创建日期", deptDTO.getCreateTime()); + list.add(map); + } + FileUtil.downloadExcel(list, response); + } + + @Override + public Set getDeleteDepts(List menuList, Set deptDtos) { + for (Dept dept : menuList) { + deptDtos.add(deptMapper.toDto(dept)); + List depts = deptRepository.findByPid(dept.getId()); + if(depts!=null && depts.size()!=0){ + getDeleteDepts(depts, deptDtos); + } + } + return deptDtos; + } + + @Override + public List getDeptChildren(List deptList) { + List list = new ArrayList<>(); + deptList.forEach(dept -> { + if (dept!=null && dept.getEnabled()) { + List depts = deptRepository.findByPid(dept.getId()); + if (depts.size() != 0) { + list.addAll(getDeptChildren(depts)); + } + list.add(dept.getId()); + } + } + ); + return list; + } + + @Override + public List getSuperior(DeptDto deptDto, List depts) { + if(deptDto.getPid() == null){ + depts.addAll(deptRepository.findByPidIsNull()); + return deptMapper.toDto(depts); + } + depts.addAll(deptRepository.findByPid(deptDto.getPid())); + return getSuperior(findById(deptDto.getPid()), depts); + } + + @Override + public Object buildTree(List deptDtos) { + Set trees = new LinkedHashSet<>(); + Set depts= new LinkedHashSet<>(); + List deptNames = deptDtos.stream().map(DeptDto::getName).collect(Collectors.toList()); + boolean isChild; + for (DeptDto deptDTO : deptDtos) { + isChild = false; + if (deptDTO.getPid() == null) { + trees.add(deptDTO); + } + for (DeptDto it : deptDtos) { + if (it.getPid() != null && deptDTO.getId().equals(it.getPid())) { + isChild = true; + if (deptDTO.getChildren() == null) { + deptDTO.setChildren(new ArrayList<>()); + } + deptDTO.getChildren().add(it); + } + } + if(isChild) { + depts.add(deptDTO); + } else if(deptDTO.getPid() != null && !deptNames.contains(findById(deptDTO.getPid()).getName())) { + depts.add(deptDTO); + } + } + + if (CollectionUtil.isEmpty(trees)) { + trees = depts; + } + Map map = new HashMap<>(2); + map.put("totalElements",deptDtos.size()); + map.put("content",CollectionUtil.isEmpty(trees)? deptDtos :trees); + return map; + } + + @Override + public void verification(Set deptDtos) { + Set deptIds = deptDtos.stream().map(DeptDto::getId).collect(Collectors.toSet()); + if(userRepository.countByDepts(deptIds) > 0){ + throw new BadRequestException("所选部门存在用户关联,请解除后再试!"); + } + if(roleRepository.countByDepts(deptIds) > 0){ + throw new BadRequestException("所选部门存在角色关联,请解除后再试!"); + } + } + + private void updateSubCnt(Long deptId){ + if(deptId != null){ + int count = deptRepository.countByPid(deptId); + deptRepository.updateSubCntById(count, deptId); + } + } + + private List deduplication(List list) { + List deptDtos = new ArrayList<>(); + for (DeptDto deptDto : list) { + boolean flag = true; + for (DeptDto dto : list) { + if (dto.getId().equals(deptDto.getPid())) { + flag = false; + break; + } + } + if (flag){ + deptDtos.add(deptDto); + } + } + return deptDtos; + } + + /** + * 清理缓存 + * @param id / + */ + public void delCaches(Long id){ + List users = userRepository.findByRoleDeptId(id); + // 删除数据权限 + redisUtils.delByKeys(CacheKey.DATA_USER, users.stream().map(User::getId).collect(Collectors.toSet())); + redisUtils.del(CacheKey.DEPT_ID + id); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/DictDetailServiceImpl.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/DictDetailServiceImpl.java new file mode 100644 index 0000000..5da4d05 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/DictDetailServiceImpl.java @@ -0,0 +1,95 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.impl; + +import lombok.RequiredArgsConstructor; +import me.zhengjie.utils.PageResult; +import me.zhengjie.modules.system.domain.Dict; +import me.zhengjie.modules.system.domain.DictDetail; +import me.zhengjie.modules.system.repository.DictRepository; +import me.zhengjie.modules.system.service.dto.DictDetailQueryCriteria; +import me.zhengjie.utils.*; +import me.zhengjie.modules.system.repository.DictDetailRepository; +import me.zhengjie.modules.system.service.DictDetailService; +import me.zhengjie.modules.system.service.dto.DictDetailDto; +import me.zhengjie.modules.system.service.mapstruct.DictDetailMapper; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.List; + +/** +* @author Zheng Jie +* @date 2019-04-10 +*/ +@Service +@RequiredArgsConstructor +@CacheConfig(cacheNames = "dict") +public class DictDetailServiceImpl implements DictDetailService { + + private final DictRepository dictRepository; + private final DictDetailRepository dictDetailRepository; + private final DictDetailMapper dictDetailMapper; + private final RedisUtils redisUtils; + + @Override + public PageResult queryAll(DictDetailQueryCriteria criteria, Pageable pageable) { + Page page = dictDetailRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder),pageable); + return PageUtil.toPage(page.map(dictDetailMapper::toDto)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void create(DictDetail resources) { + dictDetailRepository.save(resources); + // 清理缓存 + delCaches(resources); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(DictDetail resources) { + DictDetail dictDetail = dictDetailRepository.findById(resources.getId()).orElseGet(DictDetail::new); + ValidationUtil.isNull( dictDetail.getId(),"DictDetail","id",resources.getId()); + resources.setId(dictDetail.getId()); + dictDetailRepository.save(resources); + // 清理缓存 + delCaches(resources); + } + + @Override + @Cacheable(key = "'name:' + #p0") + public List getDictByName(String name) { + return dictDetailMapper.toDto(dictDetailRepository.findByDictName(name)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Long id) { + DictDetail dictDetail = dictDetailRepository.findById(id).orElseGet(DictDetail::new); + // 清理缓存 + delCaches(dictDetail); + dictDetailRepository.deleteById(id); + } + + public void delCaches(DictDetail dictDetail){ + Dict dict = dictRepository.findById(dictDetail.getDict().getId()).orElseGet(Dict::new); + redisUtils.del(CacheKey.DICT_NAME + dict.getName()); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/DictServiceImpl.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/DictServiceImpl.java new file mode 100644 index 0000000..6826512 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/DictServiceImpl.java @@ -0,0 +1,122 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import lombok.RequiredArgsConstructor; +import me.zhengjie.utils.PageResult; +import me.zhengjie.modules.system.domain.Dict; +import me.zhengjie.modules.system.service.dto.DictDetailDto; +import me.zhengjie.modules.system.service.dto.DictQueryCriteria; +import me.zhengjie.utils.*; +import me.zhengjie.modules.system.repository.DictRepository; +import me.zhengjie.modules.system.service.DictService; +import me.zhengjie.modules.system.service.dto.DictDto; +import me.zhengjie.modules.system.service.mapstruct.DictMapper; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; + +/** +* @author Zheng Jie +* @date 2019-04-10 +*/ +@Service +@RequiredArgsConstructor +@CacheConfig(cacheNames = "dict") +public class DictServiceImpl implements DictService { + + private final DictRepository dictRepository; + private final DictMapper dictMapper; + private final RedisUtils redisUtils; + + @Override + public PageResult queryAll(DictQueryCriteria dict, Pageable pageable){ + Page page = dictRepository.findAll((root, query, cb) -> QueryHelp.getPredicate(root, dict, cb), pageable); + return PageUtil.toPage(page.map(dictMapper::toDto)); + } + + @Override + public List queryAll(DictQueryCriteria dict) { + List list = dictRepository.findAll((root, query, cb) -> QueryHelp.getPredicate(root, dict, cb)); + return dictMapper.toDto(list); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void create(Dict resources) { + dictRepository.save(resources); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(Dict resources) { + // 清理缓存 + delCaches(resources); + Dict dict = dictRepository.findById(resources.getId()).orElseGet(Dict::new); + ValidationUtil.isNull( dict.getId(),"Dict","id",resources.getId()); + dict.setName(resources.getName()); + dict.setDescription(resources.getDescription()); + dictRepository.save(dict); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Set ids) { + // 清理缓存 + List dicts = dictRepository.findByIdIn(ids); + for (Dict dict : dicts) { + delCaches(dict); + } + dictRepository.deleteByIdIn(ids); + } + + @Override + public void download(List dictDtos, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (DictDto dictDTO : dictDtos) { + if(CollectionUtil.isNotEmpty(dictDTO.getDictDetails())){ + for (DictDetailDto dictDetail : dictDTO.getDictDetails()) { + Map map = new LinkedHashMap<>(); + map.put("字典名称", dictDTO.getName()); + map.put("字典描述", dictDTO.getDescription()); + map.put("字典标签", dictDetail.getLabel()); + map.put("字典值", dictDetail.getValue()); + map.put("创建日期", dictDetail.getCreateTime()); + list.add(map); + } + } else { + Map map = new LinkedHashMap<>(); + map.put("字典名称", dictDTO.getName()); + map.put("字典描述", dictDTO.getDescription()); + map.put("字典标签", null); + map.put("字典值", null); + map.put("创建日期", dictDTO.getCreateTime()); + list.add(map); + } + } + FileUtil.downloadExcel(list, response); + } + + public void delCaches(Dict dict){ + redisUtils.del(CacheKey.DICT_NAME + dict.getName()); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/JobServiceImpl.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/JobServiceImpl.java new file mode 100644 index 0000000..a608b97 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/JobServiceImpl.java @@ -0,0 +1,126 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.impl; + +import lombok.RequiredArgsConstructor; +import me.zhengjie.utils.PageResult; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.exception.EntityExistException; +import me.zhengjie.modules.system.domain.Job; +import me.zhengjie.modules.system.repository.UserRepository; +import me.zhengjie.modules.system.service.dto.JobQueryCriteria; +import me.zhengjie.utils.*; +import me.zhengjie.modules.system.repository.JobRepository; +import me.zhengjie.modules.system.service.JobService; +import me.zhengjie.modules.system.service.dto.JobDto; +import me.zhengjie.modules.system.service.mapstruct.JobMapper; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; + +/** +* @author Zheng Jie +* @date 2019-03-29 +*/ +@Service +@RequiredArgsConstructor +@CacheConfig(cacheNames = "job") +public class JobServiceImpl implements JobService { + + private final JobRepository jobRepository; + private final JobMapper jobMapper; + private final RedisUtils redisUtils; + private final UserRepository userRepository; + + @Override + public PageResult queryAll(JobQueryCriteria criteria, Pageable pageable) { + Page page = jobRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder),pageable); + return PageUtil.toPage(page.map(jobMapper::toDto).getContent(),page.getTotalElements()); + } + + @Override + public List queryAll(JobQueryCriteria criteria) { + List list = jobRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder)); + return jobMapper.toDto(list); + } + + @Override + @Cacheable(key = "'id:' + #p0") + public JobDto findById(Long id) { + Job job = jobRepository.findById(id).orElseGet(Job::new); + ValidationUtil.isNull(job.getId(),"Job","id",id); + return jobMapper.toDto(job); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void create(Job resources) { + Job job = jobRepository.findByName(resources.getName()); + if(job != null){ + throw new EntityExistException(Job.class,"name",resources.getName()); + } + jobRepository.save(resources); + } + + @Override + @CacheEvict(key = "'id:' + #p0.id") + @Transactional(rollbackFor = Exception.class) + public void update(Job resources) { + Job job = jobRepository.findById(resources.getId()).orElseGet(Job::new); + Job old = jobRepository.findByName(resources.getName()); + if(old != null && !old.getId().equals(resources.getId())){ + throw new EntityExistException(Job.class,"name",resources.getName()); + } + ValidationUtil.isNull( job.getId(),"Job","id",resources.getId()); + resources.setId(job.getId()); + jobRepository.save(resources); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Set ids) { + jobRepository.deleteAllByIdIn(ids); + // 删除缓存 + redisUtils.delByKeys(CacheKey.JOB_ID, ids); + } + + @Override + public void download(List jobDtos, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (JobDto jobDTO : jobDtos) { + Map map = new LinkedHashMap<>(); + map.put("岗位名称", jobDTO.getName()); + map.put("岗位状态", jobDTO.getEnabled() ? "启用" : "停用"); + map.put("创建日期", jobDTO.getCreateTime()); + list.add(map); + } + FileUtil.downloadExcel(list, response); + } + + @Override + public void verification(Set ids) { + if(userRepository.countByJobs(ids) > 0){ + throw new BadRequestException("所选的岗位中存在用户关联,请解除关联再试!"); + } + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/MenuServiceImpl.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/MenuServiceImpl.java new file mode 100644 index 0000000..ca8297e --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/MenuServiceImpl.java @@ -0,0 +1,359 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import lombok.RequiredArgsConstructor; +import me.zhengjie.modules.system.domain.Menu; +import me.zhengjie.modules.system.domain.Role; +import me.zhengjie.modules.system.domain.User; +import me.zhengjie.modules.system.domain.vo.MenuMetaVo; +import me.zhengjie.modules.system.domain.vo.MenuVo; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.exception.EntityExistException; +import me.zhengjie.modules.system.repository.MenuRepository; +import me.zhengjie.modules.system.repository.UserRepository; +import me.zhengjie.modules.system.service.MenuService; +import me.zhengjie.modules.system.service.RoleService; +import me.zhengjie.modules.system.service.dto.MenuDto; +import me.zhengjie.modules.system.service.dto.MenuQueryCriteria; +import me.zhengjie.modules.system.service.dto.RoleSmallDto; +import me.zhengjie.modules.system.service.mapstruct.MenuMapper; +import me.zhengjie.utils.*; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author Zheng Jie + */ +@Service +@RequiredArgsConstructor +@CacheConfig(cacheNames = "menu") +public class MenuServiceImpl implements MenuService { + + private final MenuRepository menuRepository; + private final UserRepository userRepository; + private final MenuMapper menuMapper; + private final RoleService roleService; + private final RedisUtils redisUtils; + + private static final String HTTP_PRE = "http://"; + private static final String HTTPS_PRE = "https://"; + private static final String YES_STR = "是"; + private static final String NO_STR = "否"; + private static final String BAD_REQUEST = "外链必须以http://或者https://开头"; + + @Override + public List queryAll(MenuQueryCriteria criteria, Boolean isQuery) throws Exception { + Sort sort = Sort.by(Sort.Direction.ASC, "menuSort"); + if(Boolean.TRUE.equals(isQuery)){ + criteria.setPidIsNull(true); + List fields = QueryHelp.getAllFields(criteria.getClass(), new ArrayList<>()); + for (Field field : fields) { + //设置对象的访问权限,保证对private的属性的访问 + field.setAccessible(true); + Object val = field.get(criteria); + if("pidIsNull".equals(field.getName())){ + continue; + } + if (ObjectUtil.isNotNull(val)) { + criteria.setPidIsNull(null); + break; + } + } + } + return menuMapper.toDto(menuRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder),sort)); + } + + @Override + @Cacheable(key = "'id:' + #p0") + public MenuDto findById(long id) { + Menu menu = menuRepository.findById(id).orElseGet(Menu::new); + ValidationUtil.isNull(menu.getId(),"Menu","id",id); + return menuMapper.toDto(menu); + } + + /** + * 用户角色改变时需清理缓存 + * @param currentUserId / + * @return / + */ + @Override + @Cacheable(key = "'user:' + #p0") + public List findByUser(Long currentUserId) { + List roles = roleService.findByUsersId(currentUserId); + Set roleIds = roles.stream().map(RoleSmallDto::getId).collect(Collectors.toSet()); + LinkedHashSet menus = menuRepository.findByRoleIdsAndTypeNot(roleIds, 2); + return menus.stream().map(menuMapper::toDto).collect(Collectors.toList()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void create(Menu resources) { + if(menuRepository.findByTitle(resources.getTitle()) != null){ + throw new EntityExistException(Menu.class,"title",resources.getTitle()); + } + if(StringUtils.isNotBlank(resources.getComponentName())){ + if(menuRepository.findByComponentName(resources.getComponentName()) != null){ + throw new EntityExistException(Menu.class,"componentName",resources.getComponentName()); + } + } + if (Long.valueOf(0L).equals(resources.getPid())) { + resources.setPid(null); + } + if(resources.getIFrame()){ + if (!(resources.getPath().toLowerCase().startsWith(HTTP_PRE)||resources.getPath().toLowerCase().startsWith(HTTPS_PRE))) { + throw new BadRequestException(BAD_REQUEST); + } + } + menuRepository.save(resources); + // 计算子节点数目 + resources.setSubCount(0); + // 更新父节点菜单数目 + updateSubCnt(resources.getPid()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(Menu resources) { + if(resources.getId().equals(resources.getPid())) { + throw new BadRequestException("上级不能为自己"); + } + Menu menu = menuRepository.findById(resources.getId()).orElseGet(Menu::new); + ValidationUtil.isNull(menu.getId(),"Permission","id",resources.getId()); + + if(resources.getIFrame()){ + if (!(resources.getPath().toLowerCase().startsWith(HTTP_PRE)||resources.getPath().toLowerCase().startsWith(HTTPS_PRE))) { + throw new BadRequestException(BAD_REQUEST); + } + } + Menu menu1 = menuRepository.findByTitle(resources.getTitle()); + + if(menu1 != null && !menu1.getId().equals(menu.getId())){ + throw new EntityExistException(Menu.class,"title",resources.getTitle()); + } + + if(resources.getPid().equals(0L)){ + resources.setPid(null); + } + + // 记录的父节点ID + Long oldPid = menu.getPid(); + Long newPid = resources.getPid(); + + if(StringUtils.isNotBlank(resources.getComponentName())){ + menu1 = menuRepository.findByComponentName(resources.getComponentName()); + if(menu1 != null && !menu1.getId().equals(menu.getId())){ + throw new EntityExistException(Menu.class,"componentName",resources.getComponentName()); + } + } + menu.setTitle(resources.getTitle()); + menu.setComponent(resources.getComponent()); + menu.setPath(resources.getPath()); + menu.setIcon(resources.getIcon()); + menu.setIFrame(resources.getIFrame()); + menu.setPid(resources.getPid()); + menu.setMenuSort(resources.getMenuSort()); + menu.setCache(resources.getCache()); + menu.setHidden(resources.getHidden()); + menu.setComponentName(resources.getComponentName()); + menu.setPermission(resources.getPermission()); + menu.setType(resources.getType()); + menuRepository.save(menu); + // 计算父级菜单节点数目 + updateSubCnt(oldPid); + updateSubCnt(newPid); + // 清理缓存 + delCaches(resources.getId()); + } + + @Override + public Set getChildMenus(List menuList, Set menuSet) { + for (Menu menu : menuList) { + menuSet.add(menu); + List menus = menuRepository.findByPidOrderByMenuSort(menu.getId()); + if(menus!=null && menus.size()!=0){ + getChildMenus(menus, menuSet); + } + } + return menuSet; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Set menuSet) { + for (Menu menu : menuSet) { + // 清理缓存 + delCaches(menu.getId()); + roleService.untiedMenu(menu.getId()); + menuRepository.deleteById(menu.getId()); + updateSubCnt(menu.getPid()); + } + } + + @Override + public List getMenus(Long pid) { + List menus; + if(pid != null && !pid.equals(0L)){ + menus = menuRepository.findByPidOrderByMenuSort(pid); + } else { + menus = menuRepository.findByPidIsNullOrderByMenuSort(); + } + return menuMapper.toDto(menus); + } + + @Override + public List getSuperior(MenuDto menuDto, List menus) { + if(menuDto.getPid() == null){ + menus.addAll(menuRepository.findByPidIsNullOrderByMenuSort()); + return menuMapper.toDto(menus); + } + menus.addAll(menuRepository.findByPidOrderByMenuSort(menuDto.getPid())); + return getSuperior(findById(menuDto.getPid()), menus); + } + + @Override + public List buildTree(List menuDtos) { + List trees = new ArrayList<>(); + Set ids = new HashSet<>(); + for (MenuDto menuDTO : menuDtos) { + if (menuDTO.getPid() == null) { + trees.add(menuDTO); + } + for (MenuDto it : menuDtos) { + if (menuDTO.getId().equals(it.getPid())) { + if (menuDTO.getChildren() == null) { + menuDTO.setChildren(new ArrayList<>()); + } + menuDTO.getChildren().add(it); + ids.add(it.getId()); + } + } + } + if(trees.size() == 0){ + trees = menuDtos.stream().filter(s -> !ids.contains(s.getId())).collect(Collectors.toList()); + } + return trees; + } + + @Override + public List buildMenus(List menuDtos) { + List list = new LinkedList<>(); + menuDtos.forEach(menuDTO -> { + if (menuDTO!=null){ + List menuDtoList = menuDTO.getChildren(); + MenuVo menuVo = new MenuVo(); + menuVo.setName(ObjectUtil.isNotEmpty(menuDTO.getComponentName()) ? menuDTO.getComponentName() : menuDTO.getTitle()); + // 一级目录需要加斜杠,不然会报警告 + menuVo.setPath(menuDTO.getPid() == null ? "/" + menuDTO.getPath() :menuDTO.getPath()); + menuVo.setHidden(menuDTO.getHidden()); + // 如果不是外链 + if(!menuDTO.getIFrame()){ + if(menuDTO.getPid() == null){ + menuVo.setComponent(StringUtils.isEmpty(menuDTO.getComponent())?"Layout":menuDTO.getComponent()); + // 如果不是一级菜单,并且菜单类型为目录,则代表是多级菜单 + }else if(menuDTO.getType() == 0){ + menuVo.setComponent(StringUtils.isEmpty(menuDTO.getComponent())?"ParentView":menuDTO.getComponent()); + }else if(StringUtils.isNoneBlank(menuDTO.getComponent())){ + menuVo.setComponent(menuDTO.getComponent()); + } + } + menuVo.setMeta(new MenuMetaVo(menuDTO.getTitle(),menuDTO.getIcon(),!menuDTO.getCache())); + if(CollectionUtil.isNotEmpty(menuDtoList)){ + menuVo.setAlwaysShow(true); + menuVo.setRedirect("noredirect"); + menuVo.setChildren(buildMenus(menuDtoList)); + // 处理是一级菜单并且没有子菜单的情况 + } else if(menuDTO.getPid() == null){ + MenuVo menuVo1 = new MenuVo(); + menuVo1.setMeta(menuVo.getMeta()); + // 非外链 + if(!menuDTO.getIFrame()){ + menuVo1.setPath("index"); + menuVo1.setName(menuVo.getName()); + menuVo1.setComponent(menuVo.getComponent()); + } else { + menuVo1.setPath(menuDTO.getPath()); + } + menuVo.setName(null); + menuVo.setMeta(null); + menuVo.setComponent("Layout"); + List list1 = new ArrayList<>(); + list1.add(menuVo1); + menuVo.setChildren(list1); + } + list.add(menuVo); + } + } + ); + return list; + } + + @Override + public Menu findOne(Long id) { + Menu menu = menuRepository.findById(id).orElseGet(Menu::new); + ValidationUtil.isNull(menu.getId(),"Menu","id",id); + return menu; + } + + @Override + public void download(List menuDtos, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (MenuDto menuDTO : menuDtos) { + Map map = new LinkedHashMap<>(); + map.put("菜单标题", menuDTO.getTitle()); + map.put("菜单类型", menuDTO.getType() == null ? "目录" : menuDTO.getType() == 1 ? "菜单" : "按钮"); + map.put("权限标识", menuDTO.getPermission()); + map.put("外链菜单", menuDTO.getIFrame() ? YES_STR : NO_STR); + map.put("菜单可见", menuDTO.getHidden() ? NO_STR : YES_STR); + map.put("是否缓存", menuDTO.getCache() ? YES_STR : NO_STR); + map.put("创建日期", menuDTO.getCreateTime()); + list.add(map); + } + FileUtil.downloadExcel(list, response); + } + + private void updateSubCnt(Long menuId){ + if(menuId != null){ + int count = menuRepository.countByPid(menuId); + menuRepository.updateSubCntById(count, menuId); + } + } + + /** + * 清理缓存 + * @param id 菜单ID + */ + public void delCaches(Long id){ + List users = userRepository.findByMenuId(id); + redisUtils.del(CacheKey.MENU_ID + id); + redisUtils.delByKeys(CacheKey.MENU_USER, users.stream().map(User::getId).collect(Collectors.toSet())); + // 清除 Role 缓存 + List roles = roleService.findInMenuId(new ArrayList(){{ + add(id); + }}); + redisUtils.delByKeys(CacheKey.ROLE_ID, roles.stream().map(Role::getId).collect(Collectors.toSet())); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/MonitorServiceImpl.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/MonitorServiceImpl.java new file mode 100644 index 0000000..4868bab --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/MonitorServiceImpl.java @@ -0,0 +1,192 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.impl; + +import cn.hutool.core.date.BetweenFormatter.Level; +import cn.hutool.core.date.DateUtil; +import me.zhengjie.modules.system.service.MonitorService; +import me.zhengjie.utils.ElConstant; +import me.zhengjie.utils.FileUtil; +import me.zhengjie.utils.StringUtils; +import org.springframework.stereotype.Service; +import oshi.SystemInfo; +import oshi.hardware.*; +import oshi.software.os.FileSystem; +import oshi.software.os.OSFileStore; +import oshi.software.os.OperatingSystem; +import oshi.util.FormatUtil; +import oshi.util.Util; +import java.lang.management.ManagementFactory; +import java.text.DecimalFormat; +import java.util.*; + +/** +* @author Zheng Jie +* @date 2020-05-02 +*/ +@Service +public class MonitorServiceImpl implements MonitorService { + + private final DecimalFormat df = new DecimalFormat("0.00"); + + @Override + public Map getServers(){ + Map resultMap = new LinkedHashMap<>(8); + try { + SystemInfo si = new SystemInfo(); + OperatingSystem os = si.getOperatingSystem(); + HardwareAbstractionLayer hal = si.getHardware(); + // 系统信息 + resultMap.put("sys", getSystemInfo(os)); + // cpu 信息 + resultMap.put("cpu", getCpuInfo(hal.getProcessor())); + // 内存信息 + resultMap.put("memory", getMemoryInfo(hal.getMemory())); + // 交换区信息 + resultMap.put("swap", getSwapInfo(hal.getMemory())); + // 磁盘 + resultMap.put("disk", getDiskInfo(os)); + resultMap.put("time", DateUtil.format(new Date(), "HH:mm:ss")); + } catch (Exception e) { + e.printStackTrace(); + } + return resultMap; + } + + /** + * 获取磁盘信息 + * @return / + */ + private Map getDiskInfo(OperatingSystem os) { + Map diskInfo = new LinkedHashMap<>(); + FileSystem fileSystem = os.getFileSystem(); + List fsArray = fileSystem.getFileStores(); + String osName = System.getProperty("os.name"); + long available = 0, total = 0; + for (OSFileStore fs : fsArray){ + // windows 需要将所有磁盘分区累加,linux 和 mac 直接累加会出现磁盘重复的问题,待修复 + if(osName.toLowerCase().startsWith(ElConstant.WIN)) { + available += fs.getUsableSpace(); + total += fs.getTotalSpace(); + } else { + available = fs.getUsableSpace(); + total = fs.getTotalSpace(); + break; + } + } + long used = total - available; + diskInfo.put("total", total > 0 ? FileUtil.getSize(total) : "?"); + diskInfo.put("available", FileUtil.getSize(available)); + diskInfo.put("used", FileUtil.getSize(used)); + if(total != 0){ + diskInfo.put("usageRate", df.format(used/(double)total * 100)); + } else { + diskInfo.put("usageRate", 0); + } + return diskInfo; + } + + /** + * 获取交换区信息 + * @param memory / + * @return / + */ + private Map getSwapInfo(GlobalMemory memory) { + Map swapInfo = new LinkedHashMap<>(); + VirtualMemory virtualMemory = memory.getVirtualMemory(); + long total = virtualMemory.getSwapTotal(); + long used = virtualMemory.getSwapUsed(); + swapInfo.put("total", FormatUtil.formatBytes(total)); + swapInfo.put("used", FormatUtil.formatBytes(used)); + swapInfo.put("available", FormatUtil.formatBytes(total - used)); + if(used == 0){ + swapInfo.put("usageRate", 0); + } else { + swapInfo.put("usageRate", df.format(used/(double)total * 100)); + } + return swapInfo; + } + + /** + * 获取内存信息 + * @param memory / + * @return / + */ + private Map getMemoryInfo(GlobalMemory memory) { + Map memoryInfo = new LinkedHashMap<>(); + memoryInfo.put("total", FormatUtil.formatBytes(memory.getTotal())); + memoryInfo.put("available", FormatUtil.formatBytes(memory.getAvailable())); + memoryInfo.put("used", FormatUtil.formatBytes(memory.getTotal() - memory.getAvailable())); + memoryInfo.put("usageRate", df.format((memory.getTotal() - memory.getAvailable())/(double)memory.getTotal() * 100)); + return memoryInfo; + } + + /** + * 获取Cpu相关信息 + * @param processor / + * @return / + */ + private Map getCpuInfo(CentralProcessor processor) { + Map cpuInfo = new LinkedHashMap<>(); + cpuInfo.put("name", processor.getProcessorIdentifier().getName()); + cpuInfo.put("package", processor.getPhysicalPackageCount() + "个物理CPU"); + cpuInfo.put("core", processor.getPhysicalProcessorCount() + "个物理核心"); + cpuInfo.put("coreNumber", processor.getPhysicalProcessorCount()); + cpuInfo.put("logic", processor.getLogicalProcessorCount() + "个逻辑CPU"); + // CPU信息 + long[] prevTicks = processor.getSystemCpuLoadTicks(); + // 默认等待300毫秒... + long time = 300; + Util.sleep(time); + long[] ticks = processor.getSystemCpuLoadTicks(); + while (Arrays.toString(prevTicks).equals(Arrays.toString(ticks)) && time < 1000){ + time += 25; + Util.sleep(25); + ticks = processor.getSystemCpuLoadTicks(); + } + long user = ticks[CentralProcessor.TickType.USER.getIndex()] - prevTicks[CentralProcessor.TickType.USER.getIndex()]; + long nice = ticks[CentralProcessor.TickType.NICE.getIndex()] - prevTicks[CentralProcessor.TickType.NICE.getIndex()]; + long sys = ticks[CentralProcessor.TickType.SYSTEM.getIndex()] - prevTicks[CentralProcessor.TickType.SYSTEM.getIndex()]; + long idle = ticks[CentralProcessor.TickType.IDLE.getIndex()] - prevTicks[CentralProcessor.TickType.IDLE.getIndex()]; + long iowait = ticks[CentralProcessor.TickType.IOWAIT.getIndex()] - prevTicks[CentralProcessor.TickType.IOWAIT.getIndex()]; + long irq = ticks[CentralProcessor.TickType.IRQ.getIndex()] - prevTicks[CentralProcessor.TickType.IRQ.getIndex()]; + long softirq = ticks[CentralProcessor.TickType.SOFTIRQ.getIndex()] - prevTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()]; + long steal = ticks[CentralProcessor.TickType.STEAL.getIndex()] - prevTicks[CentralProcessor.TickType.STEAL.getIndex()]; + long totalCpu = user + nice + sys + idle + iowait + irq + softirq + steal; + cpuInfo.put("used", df.format(100d * user / totalCpu + 100d * sys / totalCpu)); + cpuInfo.put("idle", df.format(100d * idle / totalCpu)); + return cpuInfo; + } + + /** + * 获取系统相关信息,系统、运行天数、系统IP + * @param os / + * @return / + */ + private Map getSystemInfo(OperatingSystem os){ + Map systemInfo = new LinkedHashMap<>(); + // jvm 运行时间 + long time = ManagementFactory.getRuntimeMXBean().getStartTime(); + Date date = new Date(time); + // 计算项目运行时间 + String formatBetween = DateUtil.formatBetween(date, new Date(), Level.HOUR); + // 系统信息 + systemInfo.put("os", os.toString()); + systemInfo.put("day", formatBetween); + systemInfo.put("ip", StringUtils.getLocalIp()); + return systemInfo; + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/PurchaseServiceImpl.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/PurchaseServiceImpl.java new file mode 100644 index 0000000..9ca5060 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/PurchaseServiceImpl.java @@ -0,0 +1,127 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.service.impl; + +import me.zhengjie.modules.system.domain.Purchase; +import me.zhengjie.utils.ValidationUtil; +import me.zhengjie.utils.FileUtil; +import lombok.RequiredArgsConstructor; +import me.zhengjie.modules.system.repository.PurchaseRepository; +import me.zhengjie.modules.system.service.PurchaseService; +import me.zhengjie.modules.system.service.dto.PurchaseDto; +import me.zhengjie.modules.system.service.dto.PurchaseQueryCriteria; +import me.zhengjie.modules.system.service.mapstruct.PurchaseMapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import me.zhengjie.utils.PageUtil; +import me.zhengjie.utils.QueryHelp; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.io.IOException; +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import me.zhengjie.utils.PageResult; + +/** +* @website https://eladmin.vip +* @description 服务实现 +* @author author +* +**/ +@Service +@RequiredArgsConstructor +public class PurchaseServiceImpl implements PurchaseService { + + private final PurchaseRepository purchaseRepository; + private final PurchaseMapper purchaseMapper; + + @Override + public PageResult queryAll(PurchaseQueryCriteria criteria, Pageable pageable){ + Page page = purchaseRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder),pageable); + return PageUtil.toPage(page.map(purchaseMapper::toDto)); + } + + @Override + public List queryAll(PurchaseQueryCriteria criteria){ + return purchaseMapper.toDto(purchaseRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder))); + } + + @Override + @Transactional + public PurchaseDto findById(Long purchaseId) { + Purchase purchase = purchaseRepository.findById(purchaseId).orElseGet(Purchase::new); + ValidationUtil.isNull(purchase.getPurchaseId(),"Purchase","purchaseId",purchaseId); + return purchaseMapper.toDto(purchase); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void create(Purchase resources) { + resources.setApplyTime(Timestamp.valueOf(LocalDateTime.now())); + purchaseRepository.save(resources); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(Purchase resources) { + Purchase purchase = purchaseRepository.findById(resources.getPurchaseId()).orElseGet(Purchase::new); + ValidationUtil.isNull( purchase.getPurchaseId(),"Purchase","id",resources.getPurchaseId()); + purchase.copy(resources); + purchaseRepository.save(purchase); + } + + @Override + public void deleteAll(Long[] ids) { + for (Long purchaseId : ids) { + purchaseRepository.deleteById(purchaseId); + } + } + + @Override + public void download(List all, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (PurchaseDto purchase : all) { + Map map = new LinkedHashMap<>(); + map.put("申请人", purchase.getUser().getNickName()); + map.put("申请事由", purchase.getReason()); + map.put("申请金额", purchase.getFee()); + map.put("货物/服务名称", purchase.getProductName()); + map.put("申请类型", purchase.getPurchaseType()); + map.put("品牌/规格/参数", purchase.getProductInfo()); + map.put("数量", purchase.getProductCount()); + map.put("预算单价", purchase.getProductPrice()); + map.put("预算总价", purchase.getTotalFee()); + map.put("资金来源", purchase.getFeeSource()); + map.put("项目名称", purchase.getProjectName()); + map.put("项目编号", purchase.getProjectNo()); + map.put("预计采购时间", purchase.getPurchaseTime()); + map.put("申请时间", purchase.getApplyTime()); + map.put("备注", purchase.getRemark()); + map.put("创建者", purchase.getCreateBy()); + map.put("更新者", purchase.getUpdateBy()); + map.put("创建日期", purchase.getCreateTime()); + map.put("更新时间", purchase.getUpdateTime()); + list.add(map); + } + FileUtil.downloadExcel(list, response); + } +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/ReimburseServiceImpl.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/ReimburseServiceImpl.java new file mode 100644 index 0000000..43a10a5 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/ReimburseServiceImpl.java @@ -0,0 +1,124 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.service.impl; + +import me.zhengjie.modules.system.domain.Reimburse; +import me.zhengjie.utils.ValidationUtil; +import me.zhengjie.utils.FileUtil; +import lombok.RequiredArgsConstructor; +import me.zhengjie.modules.system.repository.ReimburseRepository; +import me.zhengjie.modules.system.service.ReimburseService; +import me.zhengjie.modules.system.service.dto.ReimburseDto; +import me.zhengjie.modules.system.service.dto.ReimburseQueryCriteria; +import me.zhengjie.modules.system.service.mapstruct.ReimburseMapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import cn.hutool.core.lang.Snowflake; +import cn.hutool.core.util.IdUtil; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import me.zhengjie.utils.PageUtil; +import me.zhengjie.utils.QueryHelp; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.io.IOException; +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import me.zhengjie.utils.PageResult; + +/** +* @website https://eladmin.vip +* @description 服务实现 +* @author author +* +**/ +@Service +@RequiredArgsConstructor +public class ReimburseServiceImpl implements ReimburseService { + + private final ReimburseRepository reimburseRepository; + private final ReimburseMapper reimburseMapper; + + @Override + public PageResult queryAll(ReimburseQueryCriteria criteria, Pageable pageable){ + Page page = reimburseRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder),pageable); + return PageUtil.toPage(page.map(reimburseMapper::toDto)); + } + + @Override + public List queryAll(ReimburseQueryCriteria criteria){ + return reimburseMapper.toDto(reimburseRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder))); + } + + @Override + @Transactional + public ReimburseDto findById(Long reimburseId) { + Reimburse reimburse = reimburseRepository.findById(reimburseId).orElseGet(Reimburse::new); + ValidationUtil.isNull(reimburse.getReimburseId(),"Reimburse","reimburseId",reimburseId); + return reimburseMapper.toDto(reimburse); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void create(Reimburse resources) { + resources.setApplyTime(Timestamp.valueOf(LocalDateTime.now())); + reimburseRepository.save(resources); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(Reimburse resources) { + Reimburse reimburse = reimburseRepository.findById(resources.getReimburseId()).orElseGet(Reimburse::new); + ValidationUtil.isNull( reimburse.getReimburseId(),"Reimburse","id",resources.getReimburseId()); + reimburse.copy(resources); + reimburseRepository.save(reimburse); + } + + @Override + public void deleteAll(Long[] ids) { + for (Long reimburseId : ids) { + reimburseRepository.deleteById(reimburseId); + } + } + + @Override + public void download(List all, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (ReimburseDto reimburse : all) { + Map map = new LinkedHashMap<>(); + map.put("申请人", reimburse.getUser().getNickName()); + map.put("申请事由", reimburse.getReason()); + map.put("申请金额", reimburse.getFee()); + map.put("报销类型", reimburse.getReimburseType()); + map.put("项目名称", reimburse.getProjectName()); + map.put("项目编号", reimburse.getProjectNo()); + map.put("项目预算", reimburse.getProjectFee()); + map.put("花费产生时间", reimburse.getReimburseTime()); + map.put("申请时间", reimburse.getApplyTime()); + map.put("备注", reimburse.getRemark()); + map.put("创建者", reimburse.getCreateBy()); + map.put("更新者", reimburse.getUpdateBy()); + map.put("创建日期", reimburse.getCreateTime()); + map.put("更新时间", reimburse.getUpdateTime()); + list.add(map); + } + FileUtil.downloadExcel(list, response); + } +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/RoleServiceImpl.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/RoleServiceImpl.java new file mode 100644 index 0000000..e416aa8 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/RoleServiceImpl.java @@ -0,0 +1,224 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import lombok.RequiredArgsConstructor; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.modules.security.service.UserCacheManager; +import me.zhengjie.modules.security.service.dto.AuthorityDto; +import me.zhengjie.modules.system.domain.Menu; +import me.zhengjie.modules.system.domain.Role; +import me.zhengjie.exception.EntityExistException; +import me.zhengjie.modules.system.domain.User; +import me.zhengjie.modules.system.repository.RoleRepository; +import me.zhengjie.modules.system.repository.UserRepository; +import me.zhengjie.modules.system.service.RoleService; +import me.zhengjie.modules.system.service.dto.RoleDto; +import me.zhengjie.modules.system.service.dto.RoleQueryCriteria; +import me.zhengjie.modules.system.service.dto.RoleSmallDto; +import me.zhengjie.modules.system.service.dto.UserDto; +import me.zhengjie.modules.system.service.mapstruct.RoleMapper; +import me.zhengjie.modules.system.service.mapstruct.RoleSmallMapper; +import me.zhengjie.utils.*; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author Zheng Jie + * @date 2018-12-03 + */ +@Service +@RequiredArgsConstructor +@CacheConfig(cacheNames = "role") +public class RoleServiceImpl implements RoleService { + + private final RoleRepository roleRepository; + private final RoleMapper roleMapper; + private final RoleSmallMapper roleSmallMapper; + private final RedisUtils redisUtils; + private final UserRepository userRepository; + private final UserCacheManager userCacheManager; + + @Override + public List queryAll() { + Sort sort = Sort.by(Sort.Direction.ASC, "level"); + return roleMapper.toDto(roleRepository.findAll(sort)); + } + + @Override + public List queryAll(RoleQueryCriteria criteria) { + return roleMapper.toDto(roleRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root, criteria, criteriaBuilder))); + } + + @Override + public PageResult queryAll(RoleQueryCriteria criteria, Pageable pageable) { + Page page = roleRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root, criteria, criteriaBuilder), pageable); + return PageUtil.toPage(page.map(roleMapper::toDto)); + } + + @Override + @Cacheable(key = "'id:' + #p0") + @Transactional(rollbackFor = Exception.class) + public RoleDto findById(long id) { + Role role = roleRepository.findById(id).orElseGet(Role::new); + ValidationUtil.isNull(role.getId(), "Role", "id", id); + return roleMapper.toDto(role); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void create(Role resources) { + if (roleRepository.findByName(resources.getName()) != null) { + throw new EntityExistException(Role.class, "username", resources.getName()); + } + roleRepository.save(resources); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(Role resources) { + Role role = roleRepository.findById(resources.getId()).orElseGet(Role::new); + ValidationUtil.isNull(role.getId(), "Role", "id", resources.getId()); + + Role role1 = roleRepository.findByName(resources.getName()); + + if (role1 != null && !role1.getId().equals(role.getId())) { + throw new EntityExistException(Role.class, "username", resources.getName()); + } + role.setName(resources.getName()); + role.setDescription(resources.getDescription()); + role.setDataScope(resources.getDataScope()); + role.setDepts(resources.getDepts()); + role.setLevel(resources.getLevel()); + roleRepository.save(role); + // 更新相关缓存 + delCaches(role.getId(), null); + } + + @Override + public void updateMenu(Role resources, RoleDto roleDTO) { + Role role = roleMapper.toEntity(roleDTO); + List users = userRepository.findByRoleId(role.getId()); + // 更新菜单 + role.setMenus(resources.getMenus()); + delCaches(resources.getId(), users); + roleRepository.save(role); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void untiedMenu(Long menuId) { + // 更新菜单 + roleRepository.untiedMenu(menuId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Set ids) { + for (Long id : ids) { + // 更新相关缓存 + delCaches(id, null); + } + roleRepository.deleteAllByIdIn(ids); + } + + @Override + public List findByUsersId(Long id) { + return roleSmallMapper.toDto(new ArrayList<>(roleRepository.findByUserId(id))); + } + + @Override + public Integer findByRoles(Set roles) { + if (roles.size() == 0) { + return Integer.MAX_VALUE; + } + Set roleDtos = new HashSet<>(); + for (Role role : roles) { + roleDtos.add(findById(role.getId())); + } + return Collections.min(roleDtos.stream().map(RoleDto::getLevel).collect(Collectors.toList())); + } + + @Override + @Cacheable(key = "'auth:' + #p0.id") + public List mapToGrantedAuthorities(UserDto user) { + Set permissions = new HashSet<>(); + // 如果是管理员直接返回 + if (user.getIsAdmin()) { + permissions.add("admin"); + return permissions.stream().map(AuthorityDto::new) + .collect(Collectors.toList()); + } + Set roles = roleRepository.findByUserId(user.getId()); + permissions = roles.stream().flatMap(role -> role.getMenus().stream()) + .map(Menu::getPermission) + .filter(StringUtils::isNotBlank).collect(Collectors.toSet()); + return permissions.stream().map(AuthorityDto::new) + .collect(Collectors.toList()); + } + + @Override + public void download(List roles, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (RoleDto role : roles) { + Map map = new LinkedHashMap<>(); + map.put("角色名称", role.getName()); + map.put("角色级别", role.getLevel()); + map.put("描述", role.getDescription()); + map.put("创建日期", role.getCreateTime()); + list.add(map); + } + FileUtil.downloadExcel(list, response); + } + + @Override + public void verification(Set ids) { + if (userRepository.countByRoles(ids) > 0) { + throw new BadRequestException("所选角色存在用户关联,请解除关联再试!"); + } + } + + @Override + public List findInMenuId(List menuIds) { + return roleRepository.findInMenuId(menuIds); + } + + /** + * 清理缓存 + * @param id / + */ + public void delCaches(Long id, List users) { + users = CollectionUtil.isEmpty(users) ? userRepository.findByRoleId(id) : users; + if (CollectionUtil.isNotEmpty(users)) { + users.forEach(item -> userCacheManager.cleanUserCache(item.getUsername())); + Set userIds = users.stream().map(User::getId).collect(Collectors.toSet()); + redisUtils.delByKeys(CacheKey.DATA_USER, userIds); + redisUtils.delByKeys(CacheKey.MENU_USER, userIds); + redisUtils.delByKeys(CacheKey.ROLE_AUTH, userIds); + } + redisUtils.del(CacheKey.ROLE_ID + id); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/UserBankServiceImpl.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/UserBankServiceImpl.java new file mode 100644 index 0000000..d20e271 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/UserBankServiceImpl.java @@ -0,0 +1,127 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.service.impl; + +import me.zhengjie.modules.system.domain.UserBank; +import me.zhengjie.utils.ValidationUtil; +import me.zhengjie.utils.FileUtil; +import lombok.RequiredArgsConstructor; +import me.zhengjie.modules.system.repository.UserBankRepository; +import me.zhengjie.modules.system.service.UserBankService; +import me.zhengjie.modules.system.service.dto.UserBankDto; +import me.zhengjie.modules.system.service.dto.UserBankQueryCriteria; +import me.zhengjie.modules.system.service.mapstruct.UserBankMapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import me.zhengjie.utils.PageUtil; +import me.zhengjie.utils.QueryHelp; + +import java.util.*; +import java.io.IOException; +import javax.servlet.http.HttpServletResponse; + +import me.zhengjie.utils.PageResult; + +/** +* @website https://eladmin.vip +* @description 服务实现 +* @author author +* @date 2023-10-31 +**/ +@Service +@RequiredArgsConstructor +public class UserBankServiceImpl implements UserBankService { + + private final UserBankRepository userBankRepository; + private final UserBankMapper userBankMapper; + + + @Override + public PageResult queryAll(UserBankQueryCriteria criteria, Pageable pageable){ + Page page = userBankRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder),pageable); + return PageUtil.toPage(page.map(userBankMapper::toDto)); + } + + @Override + public List queryAll(UserBankQueryCriteria criteria){ + return userBankMapper.toDto(userBankRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder))); + } + + @Override + @Transactional + public UserBankDto findById(Long bankId) { + UserBank userBank = userBankRepository.findById(bankId).orElseGet(UserBank::new); + ValidationUtil.isNull(userBank.getBankId(),"UserBank","bankId",bankId); + return userBankMapper.toDto(userBank); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void create(UserBank resources) { + handleBankUse(resources); + userBankRepository.save(resources); + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(UserBank resources) { + handleBankUse(resources); + UserBank userBank = userBankRepository.findById(resources.getBankId()).orElseGet(UserBank::new); + ValidationUtil.isNull( userBank.getBankId(),"UserBank","id",resources.getBankId()); + userBank.copy(resources); + userBankRepository.save(userBank); + } + + /** + * 常用银行卡处理 + * */ + private void handleBankUse(UserBank resources) { + if (Objects.equals("是", resources.getBankUse())) { + UserBank useUserBank = userBankRepository.findByBankUseAndUser("是",resources.getUser()); + useUserBank.setBankUse("否"); + userBankRepository.save(useUserBank); + } + } + + @Override + public void deleteAll(Long[] ids) { + for (Long bankId : ids) { + userBankRepository.deleteById(bankId); + } + } + + @Override + public void download(List all, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (UserBankDto userBank : all) { + Map map = new LinkedHashMap<>(); + map.put("所属用户", userBank.getUser().getNickName()); + map.put("银行卡号", userBank.getBankNo()); + map.put("银行卡所属支行", userBank.getBankName()); + map.put("是否常用账号", userBank.getBankUse()); + map.put("备注", userBank.getRemark()); + map.put("创建者", userBank.getCreateBy()); + map.put("更新者", userBank.getUpdateBy()); + map.put("创建日期", userBank.getCreateTime()); + map.put("更新时间", userBank.getUpdateTime()); + list.add(map); + } + FileUtil.downloadExcel(list, response); + } +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/UserServiceImpl.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..9a6e543 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/UserServiceImpl.java @@ -0,0 +1,277 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.impl; + +import lombok.RequiredArgsConstructor; +import me.zhengjie.utils.PageResult; +import me.zhengjie.config.FileProperties; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.modules.security.service.OnlineUserService; +import me.zhengjie.modules.security.service.UserCacheManager; +import me.zhengjie.modules.system.domain.User; +import me.zhengjie.exception.EntityExistException; +import me.zhengjie.exception.EntityNotFoundException; +import me.zhengjie.modules.system.repository.UserRepository; +import me.zhengjie.modules.system.service.UserService; +import me.zhengjie.modules.system.service.dto.*; +import me.zhengjie.modules.system.service.mapstruct.UserLoginMapper; +import me.zhengjie.modules.system.service.mapstruct.UserMapper; +import me.zhengjie.utils.*; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletResponse; +import javax.validation.constraints.NotBlank; +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author Zheng Jie + * @date 2018-11-23 + */ +@Service +@RequiredArgsConstructor +@CacheConfig(cacheNames = "user") +public class UserServiceImpl implements UserService { + + private final UserRepository userRepository; + private final UserMapper userMapper; + private final FileProperties properties; + private final RedisUtils redisUtils; + private final UserCacheManager userCacheManager; + private final OnlineUserService onlineUserService; + private final UserLoginMapper userLoginMapper; + + @Override + public PageResult queryAll(UserQueryCriteria criteria, Pageable pageable) { + Page page = userRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root, criteria, criteriaBuilder), pageable); + return PageUtil.toPage(page.map(userMapper::toDto)); + } + + @Override + public List queryAll(UserQueryCriteria criteria) { + List users = userRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root, criteria, criteriaBuilder)); + return userMapper.toDto(users); + } + + @Override + @Cacheable(key = "'id:' + #p0") + @Transactional(rollbackFor = Exception.class) + public UserDto findById(long id) { + User user = userRepository.findById(id).orElseGet(User::new); + ValidationUtil.isNull(user.getId(), "User", "id", id); + return userMapper.toDto(user); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void create(User resources) { + if (userRepository.findByUsername(resources.getUsername()) != null) { + throw new EntityExistException(User.class, "username", resources.getUsername()); + } + if (userRepository.findByEmail(resources.getEmail()) != null) { + throw new EntityExistException(User.class, "email", resources.getEmail()); + } + if (userRepository.findByPhone(resources.getPhone()) != null) { + throw new EntityExistException(User.class, "phone", resources.getPhone()); + } + userRepository.save(resources); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(User resources) throws Exception { + User user = userRepository.findById(resources.getId()).orElseGet(User::new); + ValidationUtil.isNull(user.getId(), "User", "id", resources.getId()); + User user1 = userRepository.findByUsername(resources.getUsername()); + User user2 = userRepository.findByEmail(resources.getEmail()); + User user3 = userRepository.findByPhone(resources.getPhone()); + if (user1 != null && !user.getId().equals(user1.getId())) { + throw new EntityExistException(User.class, "username", resources.getUsername()); + } + if (user2 != null && !user.getId().equals(user2.getId())) { + throw new EntityExistException(User.class, "email", resources.getEmail()); + } + if (user3 != null && !user.getId().equals(user3.getId())) { + throw new EntityExistException(User.class, "phone", resources.getPhone()); + } + // 如果用户的角色改变 + if (!resources.getRoles().equals(user.getRoles())) { + redisUtils.del(CacheKey.DATA_USER + resources.getId()); + redisUtils.del(CacheKey.MENU_USER + resources.getId()); + redisUtils.del(CacheKey.ROLE_AUTH + resources.getId()); + } + // 修改部门会影响 数据权限 + if (!Objects.equals(resources.getDept(),user.getDept())) { + redisUtils.del(CacheKey.DATA_USER + resources.getId()); + } + // 如果用户被禁用,则清除用户登录信息 + if(!resources.getEnabled()){ + onlineUserService.kickOutForUsername(resources.getUsername()); + } + user.setUsername(resources.getUsername()); + user.setEmail(resources.getEmail()); + user.setEnabled(resources.getEnabled()); + user.setRoles(resources.getRoles()); + user.setDept(resources.getDept()); + user.setJobs(resources.getJobs()); + user.setPhone(resources.getPhone()); + user.setNickName(resources.getNickName()); + user.setGender(resources.getGender()); + userRepository.save(user); + // 清除缓存 + delCaches(user.getId(), user.getUsername()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateCenter(User resources) { + User user = userRepository.findById(resources.getId()).orElseGet(User::new); + User user1 = userRepository.findByPhone(resources.getPhone()); + if (user1 != null && !user.getId().equals(user1.getId())) { + throw new EntityExistException(User.class, "phone", resources.getPhone()); + } + user.setNickName(resources.getNickName()); + user.setPhone(resources.getPhone()); + user.setGender(resources.getGender()); + userRepository.save(user); + // 清理缓存 + delCaches(user.getId(), user.getUsername()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Set ids) { + for (Long id : ids) { + // 清理缓存 + UserDto user = findById(id); + delCaches(user.getId(), user.getUsername()); + } + userRepository.deleteAllByIdIn(ids); + } + + @Override + public UserDto findByName(String userName) { + User user = userRepository.findByUsername(userName); + if (user == null) { + throw new EntityNotFoundException(User.class, "name", userName); + } else { + return userMapper.toDto(user); + } + } + + @Override + public UserLoginDto getLoginData(String userName) { + User user = userRepository.findByUsername(userName); + if (user == null) { + throw new EntityNotFoundException(User.class, "name", userName); + } else { + return userLoginMapper.toDto(user); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updatePass(String username, String pass) { + userRepository.updatePass(username, pass, new Date()); + flushCache(username); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void resetPwd(Set ids, String pwd) { + userRepository.resetPwd(ids, pwd); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Map updateAvatar(MultipartFile multipartFile) { + // 文件大小验证 + FileUtil.checkSize(properties.getAvatarMaxSize(), multipartFile.getSize()); + // 验证文件上传的格式 + String image = "gif jpg png jpeg"; + String fileType = FileUtil.getExtensionName(multipartFile.getOriginalFilename()); + if(fileType != null && !image.contains(fileType)){ + throw new BadRequestException("文件格式错误!, 仅支持 " + image +" 格式"); + } + User user = userRepository.findByUsername(SecurityUtils.getCurrentUsername()); + String oldPath = user.getAvatarPath(); + File file = FileUtil.upload(multipartFile, properties.getPath().getAvatar()); + user.setAvatarPath(Objects.requireNonNull(file).getPath()); + user.setAvatarName(file.getName()); + userRepository.save(user); + if (StringUtils.isNotBlank(oldPath)) { + FileUtil.del(oldPath); + } + @NotBlank String username = user.getUsername(); + flushCache(username); + return new HashMap(1) {{ + put("avatar", file.getName()); + }}; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateEmail(String username, String email) { + userRepository.updateEmail(username, email); + flushCache(username); + } + + @Override + public void download(List queryAll, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (UserDto userDTO : queryAll) { + List roles = userDTO.getRoles().stream().map(RoleSmallDto::getName).collect(Collectors.toList()); + Map map = new LinkedHashMap<>(); + map.put("用户名", userDTO.getUsername()); + map.put("角色", roles); + map.put("部门", userDTO.getDept().getName()); + map.put("岗位", userDTO.getJobs().stream().map(JobSmallDto::getName).collect(Collectors.toList())); + map.put("邮箱", userDTO.getEmail()); + map.put("状态", userDTO.getEnabled() ? "启用" : "禁用"); + map.put("手机号码", userDTO.getPhone()); + map.put("修改密码的时间", userDTO.getPwdResetTime()); + map.put("创建日期", userDTO.getCreateTime()); + list.add(map); + } + FileUtil.downloadExcel(list, response); + } + + /** + * 清理缓存 + * + * @param id / + */ + public void delCaches(Long id, String username) { + redisUtils.del(CacheKey.USER_ID + id); + flushCache(username); + } + + /** + * 清理 登陆时 用户缓存信息 + * + * @param username / + */ + private void flushCache(String username) { + userCacheManager.cleanUserCache(username); + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/VerifyServiceImpl.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/VerifyServiceImpl.java new file mode 100644 index 0000000..9f8c8ac --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/impl/VerifyServiceImpl.java @@ -0,0 +1,81 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.impl; + +import cn.hutool.core.lang.Dict; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import lombok.RequiredArgsConstructor; +import me.zhengjie.domain.vo.EmailVo; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.modules.system.service.VerifyService; +import me.zhengjie.utils.RedisUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.Collections; + +/** + * @author Zheng Jie + * @date 2018-12-26 + */ +@Service +@RequiredArgsConstructor +public class VerifyServiceImpl implements VerifyService { + + @Value("${code.expiration}") + private Long expiration; + private final RedisUtils redisUtils; + + @Override + @Transactional(rollbackFor = Exception.class) + public EmailVo sendEmail(String email, String key) { + EmailVo emailVo; + String content; + String redisKey = key + email; + // 如果不存在有效的验证码,就创建一个新的 + TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("template", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = engine.getTemplate("email.ftl"); + Object oldCode = redisUtils.get(redisKey); + if(oldCode == null){ + String code = RandomUtil.randomNumbers (6); + // 存入缓存 + if(!redisUtils.set(redisKey, code, expiration)){ + throw new BadRequestException("服务异常,请联系网站负责人"); + } + content = template.render(Dict.create().set("code",code)); + emailVo = new EmailVo(Collections.singletonList(email),"ELADMIN后台管理系统",content); + // 存在就再次发送原来的验证码 + } else { + content = template.render(Dict.create().set("code",oldCode)); + emailVo = new EmailVo(Collections.singletonList(email),"ELADMIN后台管理系统",content); + } + return emailVo; + } + + @Override + public void validated(String key, String code) { + Object value = redisUtils.get(key); + if(value == null || !value.toString().equals(code)){ + throw new BadRequestException("无效验证码"); + } else { + redisUtils.del(key); + } + } +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/DeptMapper.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/DeptMapper.java new file mode 100644 index 0000000..b0fb904 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/DeptMapper.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import me.zhengjie.modules.system.domain.Dept; +import me.zhengjie.modules.system.service.dto.DeptDto; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** +* @author Zheng Jie +* @date 2019-03-25 +*/ +@Mapper(componentModel = "spring",unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface DeptMapper extends BaseMapper { +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/DeptSmallMapper.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/DeptSmallMapper.java new file mode 100644 index 0000000..7507600 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/DeptSmallMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import me.zhengjie.modules.system.domain.Dept; +import me.zhengjie.modules.system.service.dto.DeptSmallDto; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** +* @author Zheng Jie +* @date 2019-03-25 +*/ +@Mapper(componentModel = "spring",unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface DeptSmallMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/DictDetailMapper.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/DictDetailMapper.java new file mode 100644 index 0000000..3f9fe84 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/DictDetailMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import me.zhengjie.modules.system.domain.DictDetail; +import me.zhengjie.modules.system.service.dto.DictDetailDto; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** +* @author Zheng Jie +* @date 2019-04-10 +*/ +@Mapper(componentModel = "spring", uses = {DictSmallMapper.class}, unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface DictDetailMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/DictMapper.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/DictMapper.java new file mode 100644 index 0000000..1047b3b --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/DictMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import me.zhengjie.modules.system.domain.Dict; +import me.zhengjie.modules.system.service.dto.DictDto; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** +* @author Zheng Jie +* @date 2019-04-10 +*/ +@Mapper(componentModel = "spring",unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface DictMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/DictSmallMapper.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/DictSmallMapper.java new file mode 100644 index 0000000..80ccc4e --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/DictSmallMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import me.zhengjie.modules.system.domain.Dict; +import me.zhengjie.modules.system.service.dto.DictSmallDto; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** +* @author Zheng Jie +* @date 2019-04-10 +*/ +@Mapper(componentModel = "spring",unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface DictSmallMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/JobMapper.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/JobMapper.java new file mode 100644 index 0000000..3771930 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/JobMapper.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import me.zhengjie.modules.system.domain.Job; +import me.zhengjie.modules.system.service.dto.JobDto; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** +* @author Zheng Jie +* @date 2019-03-29 +*/ +@Mapper(componentModel = "spring",uses = {DeptMapper.class},unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface JobMapper extends BaseMapper { +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/JobSmallMapper.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/JobSmallMapper.java new file mode 100644 index 0000000..36a59cf --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/JobSmallMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import me.zhengjie.modules.system.domain.Job; +import me.zhengjie.modules.system.service.dto.JobSmallDto; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** +* @author Zheng Jie +* @date 2019-03-29 +*/ +@Mapper(componentModel = "spring",unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface JobSmallMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/MenuMapper.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/MenuMapper.java new file mode 100644 index 0000000..6d8bffa --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/MenuMapper.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import me.zhengjie.modules.system.domain.Menu; +import me.zhengjie.modules.system.service.dto.MenuDto; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** + * @author Zheng Jie + * @date 2018-12-17 + */ +@Mapper(componentModel = "spring",unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface MenuMapper extends BaseMapper { +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/PurchaseMapper.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/PurchaseMapper.java new file mode 100644 index 0000000..bcedf46 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/PurchaseMapper.java @@ -0,0 +1,32 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import me.zhengjie.modules.system.domain.Purchase; +import me.zhengjie.modules.system.service.dto.PurchaseDto; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** +* @website https://eladmin.vip +* @author author +* +**/ +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface PurchaseMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/ReimburseMapper.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/ReimburseMapper.java new file mode 100644 index 0000000..6d97d96 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/ReimburseMapper.java @@ -0,0 +1,32 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import me.zhengjie.modules.system.domain.Reimburse; +import me.zhengjie.modules.system.service.dto.ReimburseDto; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** +* @website https://eladmin.vip +* @author author +* +**/ +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface ReimburseMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/RoleMapper.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/RoleMapper.java new file mode 100644 index 0000000..5dc551d --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/RoleMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import me.zhengjie.modules.system.domain.Role; +import me.zhengjie.modules.system.service.dto.RoleDto; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** + * @author Zheng Jie + * @date 2018-11-23 + */ +@Mapper(componentModel = "spring", uses = {MenuMapper.class, DeptMapper.class}, unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface RoleMapper extends BaseMapper { + +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/RoleSmallMapper.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/RoleSmallMapper.java new file mode 100644 index 0000000..db034b5 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/RoleSmallMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import me.zhengjie.modules.system.domain.Role; +import me.zhengjie.modules.system.service.dto.RoleSmallDto; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** + * @author Zheng Jie + * @date 2019-5-23 + */ +@Mapper(componentModel = "spring",unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface RoleSmallMapper extends BaseMapper { + +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/UserBankMapper.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/UserBankMapper.java new file mode 100644 index 0000000..9894f7b --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/UserBankMapper.java @@ -0,0 +1,32 @@ +/* +* Copyright 2019-2020 Zheng Jie +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package me.zhengjie.modules.system.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import me.zhengjie.modules.system.domain.UserBank; +import me.zhengjie.modules.system.service.dto.UserBankDto; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** +* @website https://eladmin.vip +* @author author +* @date 2023-10-31 +**/ +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface UserBankMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/UserLoginMapper.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/UserLoginMapper.java new file mode 100644 index 0000000..fbb8813 --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/UserLoginMapper.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import me.zhengjie.modules.system.domain.User; +import me.zhengjie.modules.system.service.dto.UserLoginDto; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** + * @author Zheng Jie + * @date 2018-11-23 + */ +@Mapper(componentModel = "spring",uses = {RoleMapper.class, DeptMapper.class, JobMapper.class},unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface UserLoginMapper extends BaseMapper { +} diff --git a/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/UserMapper.java b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/UserMapper.java new file mode 100644 index 0000000..510c21b --- /dev/null +++ b/eladmin-system/src/main/java/me/zhengjie/modules/system/service/mapstruct/UserMapper.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.modules.system.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import me.zhengjie.modules.system.domain.User; +import me.zhengjie.modules.system.service.dto.UserDto; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** + * @author Zheng Jie + * @date 2018-11-23 + */ +@Mapper(componentModel = "spring",uses = {RoleMapper.class, DeptMapper.class, JobMapper.class},unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface UserMapper extends BaseMapper { +} diff --git a/eladmin-system/src/main/resources/banner.txt b/eladmin-system/src/main/resources/banner.txt new file mode 100644 index 0000000..cc460ce --- /dev/null +++ b/eladmin-system/src/main/resources/banner.txt @@ -0,0 +1,8 @@ + _ _ _ + | | | | (_) + ___| |______ __ _ __| |_ __ ___ _ _ __ + / _ | |______/ _` |/ _` | '_ ` _ \| | '_ \ + | __| | | (_| | (_| | | | | | | | | | | + \___|_| \__,_|\__,_|_| |_| |_|_|_| |_| + + :: Spring Boot :: (v2.6.4) \ No newline at end of file diff --git a/eladmin-system/src/main/resources/config/application-dev.yml b/eladmin-system/src/main/resources/config/application-dev.yml new file mode 100644 index 0000000..8413bd0 --- /dev/null +++ b/eladmin-system/src/main/resources/config/application-dev.yml @@ -0,0 +1,116 @@ +#配置数据源 +spring: + datasource: + druid: + db-type: com.alibaba.druid.pool.DruidDataSource + driverClassName: net.sf.log4jdbc.sql.jdbcapi.DriverSpy + url: jdbc:log4jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:eladmin}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false + username: ${DB_USER:root} + password: ${DB_PWD:password} + # 初始连接数 + initial-size: 5 + # 最小连接数 + min-idle: 15 + # 最大连接数 + max-active: 30 + # 超时时间(以秒数为单位) + remove-abandoned-timeout: 180 + # 获取连接超时时间 + max-wait: 3000 + # 连接有效性检测时间 + time-between-eviction-runs-millis: 60000 + # 连接在池中最小生存的时间 + min-evictable-idle-time-millis: 300000 + # 连接在池中最大生存的时间 + max-evictable-idle-time-millis: 900000 + # 指明连接是否被空闲连接回收器(如果有)进行检验.如果检测失败,则连接将被从池中去除 + test-while-idle: true + # 指明是否在从池中取出连接前进行检验,如果检验失败, 则从池中去除连接并尝试取出另一个 + test-on-borrow: true + # 是否在归还到池中前进行检验 + test-on-return: false + # 检测连接是否有效 + validation-query: select 1 + # 配置监控统计 + webStatFilter: + enabled: true + stat-view-servlet: + enabled: true + url-pattern: /druid/* + reset-enable: false + filter: + stat: + enabled: true + # 记录慢SQL + log-slow-sql: true + slow-sql-millis: 1000 + merge-sql: true + wall: + config: + multi-statement-allow: true + +# 登录相关配置 +login: + # 是否限制单用户登录 + single-login: false + # Redis用户登录缓存配置 + user-cache: + # 存活时间/秒 + idle-time: 21600 + # 验证码 + login-code: + # 验证码类型配置 查看 LoginProperties 类 + code-type: arithmetic + # 登录图形验证码有效时间/分钟 + expiration: 2 + # 验证码高度 + width: 111 + # 验证码宽度 + height: 36 + # 内容长度 + length: 2 + # 字体名称,为空则使用默认字体 + font-name: + # 字体大小 + font-size: 25 + +#jwt +jwt: + header: Authorization + # 令牌前缀 + token-start-with: Bearer + # 必须使用最少88位的Base64对该令牌进行编码 + base64-secret: ZmQ0ZGI5NjQ0MDQwY2I4MjMxY2Y3ZmI3MjdhN2ZmMjNhODViOTg1ZGE0NTBjMGM4NDA5NzYxMjdjOWMwYWRmZTBlZjlhNGY3ZTg4Y2U3YTE1ODVkZDU5Y2Y3OGYwZWE1NzUzNWQ2YjFjZDc0NGMxZWU2MmQ3MjY1NzJmNTE0MzI= + # 令牌过期时间 此处单位/毫秒 ,默认4小时,可在此网站生成 https://www.convertworld.com/zh-hans/time/milliseconds.html + token-validity-in-seconds: 14400000 + # 在线用户key + online-key: "online-token:" + # 验证码 + code-key: "captcha-code:" + # token 续期检查时间范围(默认30分钟,单位毫秒),在token即将过期的一段时间内用户操作了,则给用户的token续期 + detect: 1800000 + # 续期时间范围,默认1小时,单位毫秒 + renew: 3600000 + +#是否允许生成代码,生产环境设置为false +generator: + enabled: true + +#是否开启 swagger-ui +swagger: + enabled: true + +# 文件存储路径 +file: + mac: + path: ~/file/ + avatar: ~/avatar/ + linux: + path: /home/eladmin/file/ + avatar: /home/eladmin/avatar/ + windows: + path: D:\eladmin\file\ + avatar: D:\eladmin\avatar\ + # 文件大小 /M + maxSize: 100 + avatarMaxSize: 5 diff --git a/eladmin-system/src/main/resources/config/application-prod.yml b/eladmin-system/src/main/resources/config/application-prod.yml new file mode 100644 index 0000000..39d6a11 --- /dev/null +++ b/eladmin-system/src/main/resources/config/application-prod.yml @@ -0,0 +1,125 @@ +#配置数据源 +spring: + datasource: + druid: + db-type: com.alibaba.druid.pool.DruidDataSource + driverClassName: net.sf.log4jdbc.sql.jdbcapi.DriverSpy + url: jdbc:log4jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:eladmin}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false + username: ${DB_USER:root} + password: ${DB_PWD:123456} + # 初始连接数 + initial-size: 5 + # 最小连接数 + min-idle: 15 + # 最大连接数 + max-active: 30 + # 获取连接超时时间 + max-wait: 5000 + # 连接有效性检测时间 + time-between-eviction-runs-millis: 60000 + # 连接在池中最小生存的时间 + min-evictable-idle-time-millis: 300000 + # 连接在池中最大生存的时间 + max-evictable-idle-time-millis: 900000 + # 指明连接是否被空闲连接回收器(如果有)进行检验.如果检测失败,则连接将被从池中去除 + test-while-idle: true + # 指明是否在从池中取出连接前进行检验,如果检验失败, 则从池中去除连接并尝试取出另一个 + test-on-borrow: true + # 是否在归还到池中前进行检验 + test-on-return: false + # 检测连接是否有效 + validation-query: select 1 + # 配置监控统计 + webStatFilter: + enabled: true + stat-view-servlet: + allow: + enabled: true + # 控制台管理用户名和密码 + url-pattern: /druid/* + reset-enable: false + login-username: admin + 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 + +# 登录相关配置 +login: + # 是否限制单用户登录 + single-login: false + # Redis用户登录缓存配置 + user-cache: + # 存活时间/秒 + idle-time: 21600 + # 验证码 + login-code: + # 验证码类型配置 查看 LoginProperties 类 + code-type: arithmetic + # 登录图形验证码有效时间/分钟 + expiration: 2 + # 验证码高度 + width: 111 + # 验证码宽度 + height: 36 + # 内容长度 + length: 2 + # 字体名称,为空则使用默认字体,如遇到线上乱码,设置其他字体即可 + font-name: + # 字体大小 + font-size: 25 + +#jwt +jwt: + header: Authorization + # 令牌前缀 + token-start-with: Bearer + # 必须使用最少88位的Base64对该令牌进行编码 + base64-secret: ZmQ0ZGI5NjQ0MDQwY2I4MjMxY2Y3ZmI3MjdhN2ZmMjNhODViOTg1ZGE0NTBjMGM4NDA5NzYxMjdjOWMwYWRmZTBlZjlhNGY3ZTg4Y2U3YTE1ODVkZDU5Y2Y3OGYwZWE1NzUzNWQ2YjFjZDc0NGMxZWU2MmQ3MjY1NzJmNTE0MzI= + # 令牌过期时间 此处单位/毫秒 ,默认2小时,可在此网站生成 https://www.convertworld.com/zh-hans/time/milliseconds.html + token-validity-in-seconds: 7200000 + # 在线用户key + online-key: "online-token:" + # 验证码 + code-key: "captcha-code:" + # token 续期检查时间范围(默认30分钟,单位默认毫秒),在token即将过期的一段时间内用户操作了,则给用户的token续期 + detect: 1800000 + # 续期时间范围,默认 1小时,这里单位毫秒 + renew: 3600000 + +#是否允许生成代码,生产环境设置为false +generator: + enabled: false + +#如果生产环境要开启swagger,需要配置请求地址 +#springfox: +# documentation: +# swagger: +# v2: +# host: # 接口域名或外网ip + +#是否开启 swagger-ui +swagger: + enabled: false + +# 文件存储路径 +file: + mac: + path: ~/file/ + avatar: ~/avatar/ + linux: + path: /home/eladmin/file/ + avatar: /home/eladmin/avatar/ + windows: + path: C:\eladmin\file\ + avatar: C:\eladmin\avatar\ + # 文件大小 /M + maxSize: 100 + avatarMaxSize: 5 diff --git a/eladmin-system/src/main/resources/config/application.yml b/eladmin-system/src/main/resources/config/application.yml new file mode 100644 index 0000000..774fcec --- /dev/null +++ b/eladmin-system/src/main/resources/config/application.yml @@ -0,0 +1,60 @@ +server: + port: 8000 + compression: + enabled: true + mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json + +spring: + freemarker: + check-template-location: false + profiles: + active: dev + data: + redis: + repositories: + enabled: false +# pid: +# file: /自行指定位置/eladmin.pid + + #配置 Jpa + jpa: + hibernate: + ddl-auto: none + open-in-view: true + show-sql: true # 默认false,在日志里显示执行的sql语句 + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL5InnoDBDialect + + redis: + #数据库索引 + database: ${REDIS_DB:0} + host: ${REDIS_HOST:127.0.0.1} + port: ${REDIS_PORT:6379} + password: ${REDIS_PWD:} + #连接超时时间 + timeout: 5000 + +task: + pool: + # 核心线程池大小 + core-pool-size: 10 + # 最大线程数 + max-pool-size: 30 + # 活跃时间 + keep-alive-seconds: 60 + # 队列容量 + queue-capacity: 50 + +#七牛云 +qiniu: + # 文件大小 /M + max-size: 15 + +#邮箱验证码有效时间/秒 +code: + expiration: 300 + +#密码加密传输,前端公钥加密,后端私钥解密 +rsa: + private_key: MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEA0vfvyTdGJkdbHkB8mp0f3FE0GYP3AYPaJF7jUd1M0XxFSE2ceK3k2kw20YvQ09NJKk+OMjWQl9WitG9pB6tSCQIDAQABAkA2SimBrWC2/wvauBuYqjCFwLvYiRYqZKThUS3MZlebXJiLB+Ue/gUifAAKIg1avttUZsHBHrop4qfJCwAI0+YRAiEA+W3NK/RaXtnRqmoUUkb59zsZUBLpvZgQPfj1MhyHDz0CIQDYhsAhPJ3mgS64NbUZmGWuuNKp5coY2GIj/zYDMJp6vQIgUueLFXv/eZ1ekgz2Oi67MNCk5jeTF2BurZqNLR3MSmUCIFT3Q6uHMtsB9Eha4u7hS31tj1UWE+D+ADzp59MGnoftAiBeHT7gDMuqeJHPL4b+kC+gzV4FGTfhR9q3tTbklZkD2A== \ No newline at end of file diff --git a/eladmin-system/src/main/resources/log4jdbc.log4j2.properties b/eladmin-system/src/main/resources/log4jdbc.log4j2.properties new file mode 100644 index 0000000..302525f --- /dev/null +++ b/eladmin-system/src/main/resources/log4jdbc.log4j2.properties @@ -0,0 +1,4 @@ +# If you use SLF4J. First, you need to tell log4jdbc-log4j2 that you want to use the SLF4J logger +log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator +log4jdbc.auto.load.popular.drivers=false +log4jdbc.drivers=com.mysql.cj.jdbc.Driver \ No newline at end of file diff --git a/eladmin-system/src/main/resources/logback.xml b/eladmin-system/src/main/resources/logback.xml new file mode 100644 index 0000000..3f8f9d8 --- /dev/null +++ b/eladmin-system/src/main/resources/logback.xml @@ -0,0 +1,45 @@ + + + elAdmin + + + + + + + ${log.pattern} + ${log.charset} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/eladmin-system/src/main/resources/template/email.ftl b/eladmin-system/src/main/resources/template/email.ftl new file mode 100644 index 0000000..606d490 --- /dev/null +++ b/eladmin-system/src/main/resources/template/email.ftl @@ -0,0 +1,48 @@ + + + + + + + +
+
+

尊敬的用户,您好:

+

您正在申请邮箱验证,您的验证码为:

+

${code}

+
+
+
+ Copyright ©${.now?string("yyyy")} ELADMIN 后台管理系统 All Rights Reserved. +
+ +
+
+ + diff --git a/eladmin-system/src/main/resources/template/taskAlarm.ftl b/eladmin-system/src/main/resources/template/taskAlarm.ftl new file mode 100644 index 0000000..a29b078 --- /dev/null +++ b/eladmin-system/src/main/resources/template/taskAlarm.ftl @@ -0,0 +1,69 @@ + + + + + + + +
+
+

任务信息:

+ + + + + + + + + + + + + + + + + +
任务名称Bean名称执行方法参数内容Cron表达式描述内容
${task.jobName}${task.beanName}${task.methodName}${(task.params)!""}${task.cronExpression}${(task.description)!""}
+
+
+

异常信息:

+
+                ${msg}
+            
+
+
+
+
+ Copyright ©${.now?string("yyyy")} ELADMIN 后台管理系统 All Rights Reserved. +
+ +
+ + + diff --git a/eladmin-system/src/test/java/me/zhengjie/EladminSystemApplicationTests.java b/eladmin-system/src/test/java/me/zhengjie/EladminSystemApplicationTests.java new file mode 100644 index 0000000..d3986f0 --- /dev/null +++ b/eladmin-system/src/test/java/me/zhengjie/EladminSystemApplicationTests.java @@ -0,0 +1,16 @@ +package me.zhengjie; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class EladminSystemApplicationTests { + + @Test + public void contextLoads() { + } + + public static void main(String[] args) { + } +} + diff --git a/eladmin-tools/pom.xml b/eladmin-tools/pom.xml new file mode 100644 index 0000000..7c85d8b --- /dev/null +++ b/eladmin-tools/pom.xml @@ -0,0 +1,50 @@ + + + + eladmin + me.zhengjie + 2.7 + + 4.0.0 + + eladmin-tools + 工具模块 + + + 1.4.7 + 7.9.3 + 4.22.57.ALL + + + + + + me.zhengjie + eladmin-logging + 2.7 + + + + + javax.mail + mail + ${mail.version} + + + + + com.qiniu + qiniu-java-sdk + ${qiniu.version} + + + + + com.alipay.sdk + alipay-sdk-java + ${alipay.version} + + + diff --git a/eladmin-tools/src/main/java/me/zhengjie/config/MultipartConfig.java b/eladmin-tools/src/main/java/me/zhengjie/config/MultipartConfig.java new file mode 100644 index 0000000..cf04de5 --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/config/MultipartConfig.java @@ -0,0 +1,47 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.config; + +import org.springframework.boot.web.servlet.MultipartConfigFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import javax.servlet.MultipartConfigElement; +import java.io.File; + +/** + * @date 2018-12-28 + * @author https://blog.csdn.net/llibin1024530411/article/details/79474953 + */ +@Configuration +public class MultipartConfig { + + /** + * 文件上传临时路径 + */ + @Bean + MultipartConfigElement multipartConfigElement() { + MultipartConfigFactory factory = new MultipartConfigFactory(); + String location = System.getProperty("user.home") + "/.eladmin/file/tmp"; + File tmpFile = new File(location); + if (!tmpFile.exists()) { + if (!tmpFile.mkdirs()) { + System.out.println("create was not successful."); + } + } + factory.setLocation(location); + return factory.createMultipartConfig(); + } +} \ No newline at end of file diff --git a/eladmin-tools/src/main/java/me/zhengjie/domain/AlipayConfig.java b/eladmin-tools/src/main/java/me/zhengjie/domain/AlipayConfig.java new file mode 100644 index 0000000..def695d --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/domain/AlipayConfig.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.domain; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import java.io.Serializable; + +/** + * 支付宝配置类 + * @author Zheng Jie + * @date 2018-12-31 + */ +@Data +@Entity +@Table(name = "tool_alipay_config") +public class AlipayConfig implements Serializable { + + @Id + @Column(name = "config_id") + @ApiModelProperty(value = "ID", hidden = true) + private Long id; + + @NotBlank + @ApiModelProperty(value = "应用ID") + private String appId; + + @NotBlank + @ApiModelProperty(value = "商户私钥") + private String privateKey; + + @NotBlank + @ApiModelProperty(value = "支付宝公钥") + private String publicKey; + + @ApiModelProperty(value = "签名方式") + private String signType="RSA2"; + + @Column(name = "gateway_url") + @ApiModelProperty(value = "支付宝开放安全地址", hidden = true) + private String gatewayUrl = "https://openapi.alipaydev.com/gateway.do"; + + @ApiModelProperty(value = "编码", hidden = true) + private String charset= "utf-8"; + + @NotBlank + @ApiModelProperty(value = "异步通知地址") + private String notifyUrl; + + @NotBlank + @ApiModelProperty(value = "订单完成后返回的页面") + private String returnUrl; + + @ApiModelProperty(value = "类型") + private String format="JSON"; + + @NotBlank + @ApiModelProperty(value = "商户号") + private String sysServiceProviderId; + +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/domain/EmailConfig.java b/eladmin-tools/src/main/java/me/zhengjie/domain/EmailConfig.java new file mode 100644 index 0000000..b4fc1e6 --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/domain/EmailConfig.java @@ -0,0 +1,58 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.domain; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import java.io.Serializable; + +/** + * 邮件配置类,数据存覆盖式存入数据存 + * @author Zheng Jie + * @date 2018-12-26 + */ +@Entity +@Data +@Table(name = "tool_email_config") +public class EmailConfig implements Serializable { + + @Id + @Column(name = "config_id") + @ApiModelProperty(value = "ID", hidden = true) + private Long id; + + @NotBlank + @ApiModelProperty(value = "邮件服务器SMTP地址") + private String host; + + @NotBlank + @ApiModelProperty(value = "邮件服务器 SMTP 端口") + private String port; + + @NotBlank + @ApiModelProperty(value = "发件者用户名") + private String user; + + @NotBlank + @ApiModelProperty(value = "密码") + private String pass; + + @NotBlank + @ApiModelProperty(value = "收件人") + private String fromUser; +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/domain/LocalStorage.java b/eladmin-tools/src/main/java/me/zhengjie/domain/LocalStorage.java new file mode 100644 index 0000000..59fd0ab --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/domain/LocalStorage.java @@ -0,0 +1,73 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.domain; + +import io.swagger.annotations.ApiModelProperty; +import lombok.*; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import me.zhengjie.base.BaseEntity; +import javax.persistence.*; +import java.io.Serializable; + +/** +* @author Zheng Jie +* @date 2019-09-05 +*/ +@Getter +@Setter +@Entity +@Table(name="tool_local_storage") +@NoArgsConstructor +public class LocalStorage extends BaseEntity implements Serializable { + + @Id + @Column(name = "storage_id") + @ApiModelProperty(value = "ID", hidden = true) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ApiModelProperty(value = "真实文件名") + private String realName; + + @ApiModelProperty(value = "文件名") + private String name; + + @ApiModelProperty(value = "后缀") + private String suffix; + + @ApiModelProperty(value = "路径") + private String path; + + @ApiModelProperty(value = "类型") + private String type; + + @ApiModelProperty(value = "大小") + private String size; + + public LocalStorage(String realName,String name, String suffix, String path, String type, String size) { + this.realName = realName; + this.name = name; + this.suffix = suffix; + this.path = path; + this.type = type; + this.size = size; + } + + public void copy(LocalStorage source){ + BeanUtil.copyProperties(source,this, CopyOptions.create().setIgnoreNullValue(true)); + } +} \ No newline at end of file diff --git a/eladmin-tools/src/main/java/me/zhengjie/domain/QiniuConfig.java b/eladmin-tools/src/main/java/me/zhengjie/domain/QiniuConfig.java new file mode 100644 index 0000000..0247e67 --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/domain/QiniuConfig.java @@ -0,0 +1,69 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.domain; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import java.io.Serializable; + +/** + * 七牛云对象存储配置类 + * @author Zheng Jie + * @date 2018-12-31 + */ +@Data +@Entity +@Table(name = "tool_qiniu_config") +public class QiniuConfig implements Serializable { + + @Id + @Column(name = "config_id") + @ApiModelProperty(value = "ID") + private Long id; + + @NotBlank + @ApiModelProperty(value = "accessKey") + private String accessKey; + + @NotBlank + @ApiModelProperty(value = "secretKey") + private String secretKey; + + @NotBlank + @ApiModelProperty(value = "存储空间名称作为唯一的 Bucket 识别符") + private String bucket; + + /** + * Zone表示与机房的对应关系 + * 华东 Zone.zone0() + * 华北 Zone.zone1() + * 华南 Zone.zone2() + * 北美 Zone.zoneNa0() + * 东南亚 Zone.zoneAs0() + */ + @NotBlank + @ApiModelProperty(value = "Zone表示与机房的对应关系") + private String zone; + + @NotBlank + @ApiModelProperty(value = "外链域名,可自定义,需在七牛云绑定") + private String host; + + @ApiModelProperty(value = "空间类型:公开/私有") + private String type = "公开"; +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/domain/QiniuContent.java b/eladmin-tools/src/main/java/me/zhengjie/domain/QiniuContent.java new file mode 100644 index 0000000..db69a6a --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/domain/QiniuContent.java @@ -0,0 +1,64 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.domain; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.hibernate.annotations.UpdateTimestamp; +import javax.persistence.*; +import java.io.Serializable; +import java.sql.Timestamp; + +/** + * 上传成功后,存储结果 + * @author Zheng Jie + * @date 2018-12-31 + */ +@Data +@Entity +@Table(name = "tool_qiniu_content") +public class QiniuContent implements Serializable { + + @Id + @Column(name = "content_id") + @ApiModelProperty(value = "ID", hidden = true) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "name") + @ApiModelProperty(value = "文件名") + private String key; + + @ApiModelProperty(value = "空间名") + private String bucket; + + @ApiModelProperty(value = "大小") + private String size; + + @ApiModelProperty(value = "文件地址") + private String url; + + @ApiModelProperty(value = "文件类型") + private String suffix; + + @ApiModelProperty(value = "空间类型:公开/私有") + private String type = "公开"; + + @UpdateTimestamp + @ApiModelProperty(value = "创建或更新时间") + @Column(name = "update_time") + private Timestamp updateTime; +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/domain/vo/EmailVo.java b/eladmin-tools/src/main/java/me/zhengjie/domain/vo/EmailVo.java new file mode 100644 index 0000000..1fb759f --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/domain/vo/EmailVo.java @@ -0,0 +1,44 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.domain.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import java.util.List; + +/** + * 发送邮件时,接收参数的类 + * @author 郑杰 + * @date 2018/09/28 12:02:14 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class EmailVo { + + /** 收件人,支持多个收件人 */ + @NotEmpty + private List tos; + + @NotBlank + private String subject; + + @NotBlank + private String content; +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/domain/vo/TradeVo.java b/eladmin-tools/src/main/java/me/zhengjie/domain/vo/TradeVo.java new file mode 100644 index 0000000..01c1c2f --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/domain/vo/TradeVo.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.domain.vo; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import javax.validation.constraints.NotBlank; +import java.sql.Date; +import java.sql.Timestamp; + +/** + * 交易详情,按需应该存入数据库,这里存入数据库,仅供临时测试 + * @author Zheng Jie + * @date 2018-12-31 + */ +@Data +public class TradeVo { + + /** (必填)商品描述 */ + @NotBlank + private String body; + + /** (必填)商品名称 */ + @NotBlank + private String subject; + + /** (必填)商户订单号,应该由后台生成 */ + @ApiModelProperty(hidden = true) + private String outTradeNo; + + /** (必填)第三方订单号 */ + @ApiModelProperty(hidden = true) + private String tradeNo; + + /** (必填)价格 */ + @NotBlank + private String totalAmount; + + /** 订单状态,已支付,未支付,作废 */ + @ApiModelProperty(hidden = true) + private String state; + + /** 创建时间,存入数据库时需要 */ + @ApiModelProperty(hidden = true) + private Timestamp createTime; + + /** 作废时间,存入数据库时需要 */ + @ApiModelProperty(hidden = true) + private Date cancelTime; +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/repository/AliPayRepository.java b/eladmin-tools/src/main/java/me/zhengjie/repository/AliPayRepository.java new file mode 100644 index 0000000..61183b4 --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/repository/AliPayRepository.java @@ -0,0 +1,26 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.repository; + +import me.zhengjie.domain.AlipayConfig; +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * @author Zheng Jie + * @date 2018-12-31 + */ +public interface AliPayRepository extends JpaRepository { +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/repository/EmailRepository.java b/eladmin-tools/src/main/java/me/zhengjie/repository/EmailRepository.java new file mode 100644 index 0000000..7765602 --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/repository/EmailRepository.java @@ -0,0 +1,26 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.repository; + +import me.zhengjie.domain.EmailConfig; +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * @author Zheng Jie + * @date 2018-12-26 + */ +public interface EmailRepository extends JpaRepository { +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/repository/LocalStorageRepository.java b/eladmin-tools/src/main/java/me/zhengjie/repository/LocalStorageRepository.java new file mode 100644 index 0000000..8c1e85a --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/repository/LocalStorageRepository.java @@ -0,0 +1,27 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.repository; + +import me.zhengjie.domain.LocalStorage; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** +* @author Zheng Jie +* @date 2019-09-05 +*/ +public interface LocalStorageRepository extends JpaRepository, JpaSpecificationExecutor { +} \ No newline at end of file diff --git a/eladmin-tools/src/main/java/me/zhengjie/repository/QiNiuConfigRepository.java b/eladmin-tools/src/main/java/me/zhengjie/repository/QiNiuConfigRepository.java new file mode 100644 index 0000000..9379f55 --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/repository/QiNiuConfigRepository.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.repository; + +import me.zhengjie.domain.QiniuConfig; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; + +/** + * @author Zheng Jie + * @date 2018-12-31 + */ +public interface QiNiuConfigRepository extends JpaRepository { + + /** + * 编辑类型 + * @param type + */ + @Modifying + @Query(value = "update QiniuConfig set type = ?1") + void update(String type); +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/repository/QiniuContentRepository.java b/eladmin-tools/src/main/java/me/zhengjie/repository/QiniuContentRepository.java new file mode 100644 index 0000000..55f813f --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/repository/QiniuContentRepository.java @@ -0,0 +1,34 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.repository; + +import me.zhengjie.domain.QiniuContent; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** + * @author Zheng Jie + * @date 2018-12-31 + */ +public interface QiniuContentRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * 根据key查询 + * @param key 文件名 + * @return QiniuContent + */ + QiniuContent findByKey(String key); +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/rest/AliPayController.java b/eladmin-tools/src/main/java/me/zhengjie/rest/AliPayController.java new file mode 100644 index 0000000..0c03d0a --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/rest/AliPayController.java @@ -0,0 +1,135 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.zhengjie.annotation.AnonymousAccess; +import me.zhengjie.annotation.Log; +import me.zhengjie.annotation.rest.AnonymousGetMapping; +import me.zhengjie.domain.vo.TradeVo; +import me.zhengjie.domain.AlipayConfig; +import me.zhengjie.utils.AliPayStatusEnum; +import me.zhengjie.utils.AlipayUtils; +import me.zhengjie.service.AliPayService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import springfox.documentation.annotations.ApiIgnore; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +/** + * @author Zheng Jie + * @date 2018-12-31 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/aliPay") +@Api(tags = "工具:支付宝管理") +public class AliPayController { + + private final AlipayUtils alipayUtils; + private final AliPayService alipayService; + + @GetMapping + public ResponseEntity queryAliConfig() { + return new ResponseEntity<>(alipayService.find(), HttpStatus.OK); + } + + @Log("配置支付宝") + @ApiOperation("配置支付宝") + @PutMapping + public ResponseEntity updateAliPayConfig(@Validated @RequestBody AlipayConfig alipayConfig) { + alipayService.config(alipayConfig); + return new ResponseEntity<>(HttpStatus.OK); + } + + @Log("支付宝PC网页支付") + @ApiOperation("PC网页支付") + @PostMapping(value = "/toPayAsPC") + public ResponseEntity toPayAsPc(@Validated @RequestBody TradeVo trade) throws Exception { + AlipayConfig aliPay = alipayService.find(); + trade.setOutTradeNo(alipayUtils.getOrderCode()); + String payUrl = alipayService.toPayAsPc(aliPay, trade); + return ResponseEntity.ok(payUrl); + } + + @Log("支付宝手机网页支付") + @ApiOperation("手机网页支付") + @PostMapping(value = "/toPayAsWeb") + public ResponseEntity toPayAsWeb(@Validated @RequestBody TradeVo trade) throws Exception { + AlipayConfig alipay = alipayService.find(); + trade.setOutTradeNo(alipayUtils.getOrderCode()); + String payUrl = alipayService.toPayAsWeb(alipay, trade); + return ResponseEntity.ok(payUrl); + } + + @ApiIgnore + @AnonymousGetMapping("/return") + @ApiOperation("支付之后跳转的链接") + public ResponseEntity returnPage(HttpServletRequest request, HttpServletResponse response) { + AlipayConfig alipay = alipayService.find(); + response.setContentType("text/html;charset=" + alipay.getCharset()); + //内容验签,防止黑客篡改参数 + if (alipayUtils.rsaCheck(request, alipay)) { + //商户订单号 + String outTradeNo = new String(request.getParameter("out_trade_no").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8); + //支付宝交易号 + String tradeNo = new String(request.getParameter("trade_no").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8); + System.out.println("商户订单号" + outTradeNo + " " + "第三方交易号" + tradeNo); + + // 根据业务需要返回数据,这里统一返回OK + return new ResponseEntity<>("payment successful", HttpStatus.OK); + } else { + // 根据业务需要返回数据 + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + } + + @ApiIgnore + @RequestMapping("/notify") + @AnonymousAccess + @ApiOperation("支付异步通知(要公网访问),接收异步通知,检查通知内容app_id、out_trade_no、total_amount是否与请求中的一致,根据trade_status进行后续业务处理") + public ResponseEntity notify(HttpServletRequest request) { + AlipayConfig alipay = alipayService.find(); + Map parameterMap = request.getParameterMap(); + //内容验签,防止黑客篡改参数 + if (alipayUtils.rsaCheck(request, alipay)) { + //交易状态 + String tradeStatus = new String(request.getParameter("trade_status").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8); + // 商户订单号 + String outTradeNo = new String(request.getParameter("out_trade_no").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8); + //支付宝交易号 + String tradeNo = new String(request.getParameter("trade_no").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8); + //付款金额 + String totalAmount = new String(request.getParameter("total_amount").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8); + //验证 + if (tradeStatus.equals(AliPayStatusEnum.SUCCESS.getValue()) || tradeStatus.equals(AliPayStatusEnum.FINISHED.getValue())) { + // 验证通过后应该根据业务需要处理订单 + } + return new ResponseEntity<>(HttpStatus.OK); + } + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/rest/EmailController.java b/eladmin-tools/src/main/java/me/zhengjie/rest/EmailController.java new file mode 100644 index 0000000..eb7832c --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/rest/EmailController.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import me.zhengjie.annotation.Log; +import me.zhengjie.domain.vo.EmailVo; +import me.zhengjie.domain.EmailConfig; +import me.zhengjie.service.EmailService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + * 发送邮件 + * @author 郑杰 + * @date 2018/09/28 6:55:53 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("api/email") +@Api(tags = "工具:邮件管理") +public class EmailController { + + private final EmailService emailService; + + @GetMapping + public ResponseEntity queryEmailConfig(){ + return new ResponseEntity<>(emailService.find(),HttpStatus.OK); + } + + @Log("配置邮件") + @PutMapping + @ApiOperation("配置邮件") + public ResponseEntity updateEmailConfig(@Validated @RequestBody EmailConfig emailConfig) throws Exception { + emailService.config(emailConfig,emailService.find()); + return new ResponseEntity<>(HttpStatus.OK); + } + + @Log("发送邮件") + @PostMapping + @ApiOperation("发送邮件") + public ResponseEntity sendEmail(@Validated @RequestBody EmailVo emailVo){ + emailService.send(emailVo,emailService.find()); + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/rest/LocalStorageController.java b/eladmin-tools/src/main/java/me/zhengjie/rest/LocalStorageController.java new file mode 100644 index 0000000..2b61260 --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/rest/LocalStorageController.java @@ -0,0 +1,100 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.rest; + +import lombok.RequiredArgsConstructor; +import me.zhengjie.annotation.Log; +import me.zhengjie.domain.LocalStorage; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.service.LocalStorageService; +import me.zhengjie.service.dto.LocalStorageDto; +import me.zhengjie.service.dto.LocalStorageQueryCriteria; +import me.zhengjie.utils.FileUtil; +import me.zhengjie.utils.PageResult; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import io.swagger.annotations.*; +import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** +* @author Zheng Jie +* @date 2019-09-05 +*/ +@RestController +@RequiredArgsConstructor +@Api(tags = "工具:本地存储管理") +@RequestMapping("/api/localStorage") +public class LocalStorageController { + + private final LocalStorageService localStorageService; + + @GetMapping + @ApiOperation("查询文件") + @PreAuthorize("@el.check('storage:list')") + public ResponseEntity> queryFile(LocalStorageQueryCriteria criteria, Pageable pageable){ + return new ResponseEntity<>(localStorageService.queryAll(criteria,pageable),HttpStatus.OK); + } + + @ApiOperation("导出数据") + @GetMapping(value = "/download") + @PreAuthorize("@el.check('storage:list')") + public void exportFile(HttpServletResponse response, LocalStorageQueryCriteria criteria) throws IOException { + localStorageService.download(localStorageService.queryAll(criteria), response); + } + + @PostMapping + @ApiOperation("上传文件") + @PreAuthorize("@el.check('storage:add')") + public ResponseEntity createFile(@RequestParam String name, @RequestParam("file") MultipartFile file){ + localStorageService.create(name, file); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @ApiOperation("上传图片") + @PostMapping("/pictures") + public ResponseEntity uploadPicture(@RequestParam MultipartFile file){ + // 判断文件是否为图片 + String suffix = FileUtil.getExtensionName(file.getOriginalFilename()); + if(!FileUtil.IMAGE.equals(FileUtil.getFileType(suffix))){ + throw new BadRequestException("只能上传图片"); + } + LocalStorage localStorage = localStorageService.create(null, file); + return new ResponseEntity<>(localStorage, HttpStatus.OK); + } + + @PutMapping + @Log("修改文件") + @ApiOperation("修改文件") + @PreAuthorize("@el.check('storage:edit')") + public ResponseEntity updateFile(@Validated @RequestBody LocalStorage resources){ + localStorageService.update(resources); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @Log("删除文件") + @DeleteMapping + @ApiOperation("多选删除") + public ResponseEntity deleteFile(@RequestBody Long[] ids) { + localStorageService.deleteAll(ids); + return new ResponseEntity<>(HttpStatus.OK); + } +} \ No newline at end of file diff --git a/eladmin-tools/src/main/java/me/zhengjie/rest/QiniuController.java b/eladmin-tools/src/main/java/me/zhengjie/rest/QiniuController.java new file mode 100644 index 0000000..2988c72 --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/rest/QiniuController.java @@ -0,0 +1,122 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.rest; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.zhengjie.annotation.Log; +import me.zhengjie.domain.QiniuConfig; +import me.zhengjie.domain.QiniuContent; +import me.zhengjie.service.dto.QiniuQueryCriteria; +import me.zhengjie.service.QiNiuService; +import me.zhengjie.utils.PageResult; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * 发送邮件 + * @author 郑杰 + * @date 2018/09/28 6:55:53 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/qiNiuContent") +@Api(tags = "工具:七牛云存储管理") +public class QiniuController { + + private final QiNiuService qiNiuService; + + @GetMapping(value = "/config") + public ResponseEntity queryQiNiuConfig(){ + return new ResponseEntity<>(qiNiuService.find(), HttpStatus.OK); + } + + @Log("配置七牛云存储") + @ApiOperation("配置七牛云存储") + @PutMapping(value = "/config") + public ResponseEntity updateQiNiuConfig(@Validated @RequestBody QiniuConfig qiniuConfig){ + qiNiuService.config(qiniuConfig); + qiNiuService.update(qiniuConfig.getType()); + return new ResponseEntity<>(HttpStatus.OK); + } + + @ApiOperation("导出数据") + @GetMapping(value = "/download") + public void exportQiNiu(HttpServletResponse response, QiniuQueryCriteria criteria) throws IOException { + qiNiuService.downloadList(qiNiuService.queryAll(criteria), response); + } + + @ApiOperation("查询文件") + @GetMapping + public ResponseEntity> queryQiNiu(QiniuQueryCriteria criteria, Pageable pageable){ + return new ResponseEntity<>(qiNiuService.queryAll(criteria,pageable),HttpStatus.OK); + } + + @ApiOperation("上传文件") + @PostMapping + public ResponseEntity uploadQiNiu(@RequestParam MultipartFile file){ + QiniuContent qiniuContent = qiNiuService.upload(file,qiNiuService.find()); + Map map = new HashMap<>(3); + map.put("id",qiniuContent.getId()); + map.put("errno",0); + map.put("data",new String[]{qiniuContent.getUrl()}); + return new ResponseEntity<>(map,HttpStatus.OK); + } + + @Log("同步七牛云数据") + @ApiOperation("同步七牛云数据") + @PostMapping(value = "/synchronize") + public ResponseEntity synchronizeQiNiu(){ + qiNiuService.synchronize(qiNiuService.find()); + return new ResponseEntity<>(HttpStatus.OK); + } + + @Log("下载文件") + @ApiOperation("下载文件") + @GetMapping(value = "/download/{id}") + public ResponseEntity downloadQiNiu(@PathVariable Long id){ + Map map = new HashMap<>(1); + map.put("url", qiNiuService.download(qiNiuService.findByContentId(id),qiNiuService.find())); + return new ResponseEntity<>(map,HttpStatus.OK); + } + + @Log("删除文件") + @ApiOperation("删除文件") + @DeleteMapping(value = "/{id}") + public ResponseEntity deleteQiNiu(@PathVariable Long id){ + qiNiuService.delete(qiNiuService.findByContentId(id),qiNiuService.find()); + return new ResponseEntity<>(HttpStatus.OK); + } + + @Log("删除多张图片") + @ApiOperation("删除多张图片") + @DeleteMapping + public ResponseEntity deleteAllQiNiu(@RequestBody Long[] ids) { + qiNiuService.deleteAll(ids, qiNiuService.find()); + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/service/AliPayService.java b/eladmin-tools/src/main/java/me/zhengjie/service/AliPayService.java new file mode 100644 index 0000000..be19c90 --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/service/AliPayService.java @@ -0,0 +1,57 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service; + +import me.zhengjie.domain.vo.TradeVo; +import me.zhengjie.domain.AlipayConfig; + +/** + * @author Zheng Jie + * @date 2018-12-31 + */ +public interface AliPayService { + + /** + * 查询配置 + * @return AlipayConfig + */ + AlipayConfig find(); + + /** + * 更新配置 + * @param alipayConfig 支付宝配置 + * @return AlipayConfig + */ + AlipayConfig config(AlipayConfig alipayConfig); + + /** + * 处理来自PC的交易请求 + * @param alipay 支付宝配置 + * @param trade 交易详情 + * @return String + * @throws Exception 异常 + */ + String toPayAsPc(AlipayConfig alipay, TradeVo trade) throws Exception; + + /** + * 处理来自手机网页的交易请求 + * @param alipay 支付宝配置 + * @param trade 交易详情 + * @return String + * @throws Exception 异常 + */ + String toPayAsWeb(AlipayConfig alipay, TradeVo trade) throws Exception; +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/service/EmailService.java b/eladmin-tools/src/main/java/me/zhengjie/service/EmailService.java new file mode 100644 index 0000000..d62cadc --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/service/EmailService.java @@ -0,0 +1,48 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service; + +import me.zhengjie.domain.vo.EmailVo; +import me.zhengjie.domain.EmailConfig; + +/** + * @author Zheng Jie + * @date 2018-12-26 + */ +public interface EmailService { + + /** + * 更新邮件配置 + * @param emailConfig 邮箱配置 + * @param old / + * @return / + * @throws Exception / + */ + EmailConfig config(EmailConfig emailConfig, EmailConfig old) throws Exception; + + /** + * 查询配置 + * @return EmailConfig 邮件配置 + */ + EmailConfig find(); + + /** + * 发送邮件 + * @param emailVo 邮件发送的内容 + * @param emailConfig 邮件配置 + */ + void send(EmailVo emailVo, EmailConfig emailConfig); +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/service/LocalStorageService.java b/eladmin-tools/src/main/java/me/zhengjie/service/LocalStorageService.java new file mode 100644 index 0000000..71c57d2 --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/service/LocalStorageService.java @@ -0,0 +1,83 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service; + +import me.zhengjie.domain.LocalStorage; +import me.zhengjie.service.dto.LocalStorageDto; +import me.zhengjie.service.dto.LocalStorageQueryCriteria; +import me.zhengjie.utils.PageResult; +import org.springframework.data.domain.Pageable; +import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; + +/** +* @author Zheng Jie +* @date 2019-09-05 +*/ +public interface LocalStorageService { + + /** + * 分页查询 + * @param criteria 条件 + * @param pageable 分页参数 + * @return / + */ + PageResult queryAll(LocalStorageQueryCriteria criteria, Pageable pageable); + + /** + * 查询全部数据 + * @param criteria 条件 + * @return / + */ + List queryAll(LocalStorageQueryCriteria criteria); + + /** + * 根据ID查询 + * @param id / + * @return / + */ + LocalStorageDto findById(Long id); + + /** + * 上传 + * @param name 文件名称 + * @param file 文件 + * @return + */ + LocalStorage create(String name, MultipartFile file); + + /** + * 编辑 + * @param resources 文件信息 + */ + void update(LocalStorage resources); + + /** + * 多选删除 + * @param ids / + */ + void deleteAll(Long[] ids); + + /** + * 导出数据 + * @param localStorageDtos 待导出的数据 + * @param response / + * @throws IOException / + */ + void download(List localStorageDtos, HttpServletResponse response) throws IOException; +} \ No newline at end of file diff --git a/eladmin-tools/src/main/java/me/zhengjie/service/QiNiuService.java b/eladmin-tools/src/main/java/me/zhengjie/service/QiNiuService.java new file mode 100644 index 0000000..5321208 --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/service/QiNiuService.java @@ -0,0 +1,119 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service; + +import me.zhengjie.domain.QiniuConfig; +import me.zhengjie.domain.QiniuContent; +import me.zhengjie.service.dto.QiniuQueryCriteria; +import me.zhengjie.utils.PageResult; +import org.springframework.data.domain.Pageable; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; + +/** + * @author Zheng Jie + * @date 2018-12-31 + */ +public interface QiNiuService { + + /** + * 查配置 + * @return QiniuConfig + */ + QiniuConfig find(); + + /** + * 修改配置 + * @param qiniuConfig 配置 + * @return QiniuConfig + */ + QiniuConfig config(QiniuConfig qiniuConfig); + + /** + * 分页查询 + * @param criteria 条件 + * @param pageable 分页参数 + * @return / + */ + PageResult queryAll(QiniuQueryCriteria criteria, Pageable pageable); + + /** + * 查询全部 + * @param criteria 条件 + * @return / + */ + List queryAll(QiniuQueryCriteria criteria); + + /** + * 上传文件 + * @param file 文件 + * @param qiniuConfig 配置 + * @return QiniuContent + */ + QiniuContent upload(MultipartFile file, QiniuConfig qiniuConfig); + + /** + * 查询文件 + * @param id 文件ID + * @return QiniuContent + */ + QiniuContent findByContentId(Long id); + + /** + * 下载文件 + * @param content 文件信息 + * @param config 配置 + * @return String + */ + String download(QiniuContent content, QiniuConfig config); + + /** + * 删除文件 + * @param content 文件 + * @param config 配置 + */ + void delete(QiniuContent content, QiniuConfig config); + + /** + * 同步数据 + * @param config 配置 + */ + void synchronize(QiniuConfig config); + + /** + * 删除文件 + * @param ids 文件ID数组 + * @param config 配置 + */ + void deleteAll(Long[] ids, QiniuConfig config); + + /** + * 更新数据 + * @param type 类型 + */ + void update(String type); + + /** + * 导出数据 + * @param queryAll / + * @param response / + * @throws IOException / + */ + void downloadList(List queryAll, HttpServletResponse response) throws IOException; +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/service/dto/LocalStorageDto.java b/eladmin-tools/src/main/java/me/zhengjie/service/dto/LocalStorageDto.java new file mode 100644 index 0000000..14221c2 --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/service/dto/LocalStorageDto.java @@ -0,0 +1,42 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service.dto; + +import lombok.Getter; +import lombok.Setter; +import me.zhengjie.base.BaseDTO; +import java.io.Serializable; + +/** +* @author Zheng Jie +* @date 2019-09-05 +*/ +@Getter +@Setter +public class LocalStorageDto extends BaseDTO implements Serializable { + + private Long id; + + private String realName; + + private String name; + + private String suffix; + + private String type; + + private String size; +} \ No newline at end of file diff --git a/eladmin-tools/src/main/java/me/zhengjie/service/dto/LocalStorageQueryCriteria.java b/eladmin-tools/src/main/java/me/zhengjie/service/dto/LocalStorageQueryCriteria.java new file mode 100644 index 0000000..bea1cc7 --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/service/dto/LocalStorageQueryCriteria.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service.dto; + +import lombok.Data; +import java.sql.Timestamp; +import java.util.List; + +import me.zhengjie.annotation.Query; + +/** +* @author Zheng Jie +* @date 2019-09-05 +*/ +@Data +public class LocalStorageQueryCriteria{ + + @Query(blurry = "name,suffix,type,createBy,size") + private String blurry; + + @Query(type = Query.Type.BETWEEN) + private List createTime; +} \ No newline at end of file diff --git a/eladmin-tools/src/main/java/me/zhengjie/service/dto/PictureQueryCriteria.java b/eladmin-tools/src/main/java/me/zhengjie/service/dto/PictureQueryCriteria.java new file mode 100644 index 0000000..e7d4f1b --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/service/dto/PictureQueryCriteria.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service.dto; + +import lombok.Data; +import me.zhengjie.annotation.Query; +import java.sql.Timestamp; +import java.util.List; + +/** + * sm.ms图床 + * + * @author Zheng Jie + * @date 2019-6-4 09:52:09 + */ +@Data +public class PictureQueryCriteria{ + + @Query(type = Query.Type.INNER_LIKE) + private String filename; + + @Query(type = Query.Type.INNER_LIKE) + private String username; + + @Query(type = Query.Type.BETWEEN) + private List createTime; +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/service/dto/QiniuQueryCriteria.java b/eladmin-tools/src/main/java/me/zhengjie/service/dto/QiniuQueryCriteria.java new file mode 100644 index 0000000..f5c2240 --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/service/dto/QiniuQueryCriteria.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service.dto; + +import lombok.Data; +import me.zhengjie.annotation.Query; + +import java.sql.Timestamp; +import java.util.List; + +/** + * @author Zheng Jie + * @date 2019-6-4 09:54:37 + */ +@Data +public class QiniuQueryCriteria{ + + @Query(type = Query.Type.INNER_LIKE) + private String key; + + @Query(type = Query.Type.BETWEEN) + private List createTime; +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/service/impl/AliPayServiceImpl.java b/eladmin-tools/src/main/java/me/zhengjie/service/impl/AliPayServiceImpl.java new file mode 100644 index 0000000..0625ba4 --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/service/impl/AliPayServiceImpl.java @@ -0,0 +1,119 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service.impl; + +import com.alipay.api.AlipayClient; +import com.alipay.api.DefaultAlipayClient; +import com.alipay.api.request.AlipayTradePagePayRequest; +import com.alipay.api.request.AlipayTradeWapPayRequest; +import lombok.RequiredArgsConstructor; +import me.zhengjie.domain.vo.TradeVo; +import me.zhengjie.domain.AlipayConfig; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.repository.AliPayRepository; +import me.zhengjie.service.AliPayService; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; + +/** + * @author Zheng Jie + * @date 2018-12-31 + */ +@Service +@RequiredArgsConstructor +@CacheConfig(cacheNames = "aliPay") +public class AliPayServiceImpl implements AliPayService { + + private final AliPayRepository alipayRepository; + + @Override + @Cacheable(key = "'config'") + public AlipayConfig find() { + Optional alipayConfig = alipayRepository.findById(1L); + return alipayConfig.orElseGet(AlipayConfig::new); + } + + @Override + @CachePut(key = "'config'") + @Transactional(rollbackFor = Exception.class) + public AlipayConfig config(AlipayConfig alipayConfig) { + alipayConfig.setId(1L); + return alipayRepository.save(alipayConfig); + } + + @Override + public String toPayAsPc(AlipayConfig alipay, TradeVo trade) throws Exception { + + if(alipay.getId() == null){ + throw new BadRequestException("请先添加相应配置,再操作"); + } + AlipayClient alipayClient = new DefaultAlipayClient(alipay.getGatewayUrl(), alipay.getAppId(), alipay.getPrivateKey(), alipay.getFormat(), alipay.getCharset(), alipay.getPublicKey(), alipay.getSignType()); + + // 创建API对应的request(电脑网页版) + AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); + + // 订单完成后返回的页面和异步通知地址 + request.setReturnUrl(alipay.getReturnUrl()); + request.setNotifyUrl(alipay.getNotifyUrl()); + // 填充订单参数 + request.setBizContent("{" + + " \"out_trade_no\":\""+trade.getOutTradeNo()+"\"," + + " \"product_code\":\"FAST_INSTANT_TRADE_PAY\"," + + " \"total_amount\":"+trade.getTotalAmount()+"," + + " \"subject\":\""+trade.getSubject()+"\"," + + " \"body\":\""+trade.getBody()+"\"," + + " \"extend_params\":{" + + " \"sys_service_provider_id\":\""+alipay.getSysServiceProviderId()+"\"" + + " }"+ + " }");//填充业务参数 + // 调用SDK生成表单, 通过GET方式,口可以获取url + return alipayClient.pageExecute(request, "GET").getBody(); + + } + + @Override + public String toPayAsWeb(AlipayConfig alipay, TradeVo trade) throws Exception { + if(alipay.getId() == null){ + throw new BadRequestException("请先添加相应配置,再操作"); + } + AlipayClient alipayClient = new DefaultAlipayClient(alipay.getGatewayUrl(), alipay.getAppId(), alipay.getPrivateKey(), alipay.getFormat(), alipay.getCharset(), alipay.getPublicKey(), alipay.getSignType()); + + double money = Double.parseDouble(trade.getTotalAmount()); + double maxMoney = 5000; + if(money <= 0 || money >= maxMoney){ + throw new BadRequestException("测试金额过大"); + } + // 创建API对应的request(手机网页版) + AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest(); + request.setReturnUrl(alipay.getReturnUrl()); + request.setNotifyUrl(alipay.getNotifyUrl()); + request.setBizContent("{" + + " \"out_trade_no\":\""+trade.getOutTradeNo()+"\"," + + " \"product_code\":\"FAST_INSTANT_TRADE_PAY\"," + + " \"total_amount\":"+trade.getTotalAmount()+"," + + " \"subject\":\""+trade.getSubject()+"\"," + + " \"body\":\""+trade.getBody()+"\"," + + " \"extend_params\":{" + + " \"sys_service_provider_id\":\""+alipay.getSysServiceProviderId()+"\"" + + " }"+ + " }"); + return alipayClient.pageExecute(request, "GET").getBody(); + } +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/service/impl/EmailServiceImpl.java b/eladmin-tools/src/main/java/me/zhengjie/service/impl/EmailServiceImpl.java new file mode 100644 index 0000000..aa56732 --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/service/impl/EmailServiceImpl.java @@ -0,0 +1,107 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service.impl; + +import cn.hutool.extra.mail.Mail; +import cn.hutool.extra.mail.MailAccount; +import lombok.RequiredArgsConstructor; +import me.zhengjie.domain.EmailConfig; +import me.zhengjie.domain.vo.EmailVo; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.repository.EmailRepository; +import me.zhengjie.service.EmailService; +import me.zhengjie.utils.EncryptUtils; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; + +/** + * @author Zheng Jie + * @date 2018-12-26 + */ +@Service +@RequiredArgsConstructor +@CacheConfig(cacheNames = "email") +public class EmailServiceImpl implements EmailService { + + private final EmailRepository emailRepository; + + @Override + @CachePut(key = "'config'") + @Transactional(rollbackFor = Exception.class) + public EmailConfig config(EmailConfig emailConfig, EmailConfig old) throws Exception { + emailConfig.setId(1L); + if(!emailConfig.getPass().equals(old.getPass())){ + // 对称加密 + emailConfig.setPass(EncryptUtils.desEncrypt(emailConfig.getPass())); + } + return emailRepository.save(emailConfig); + } + + @Override + @Cacheable(key = "'config'") + public EmailConfig find() { + Optional emailConfig = emailRepository.findById(1L); + return emailConfig.orElseGet(EmailConfig::new); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void send(EmailVo emailVo, EmailConfig emailConfig){ + if(emailConfig.getId() == null){ + throw new BadRequestException("请先配置,再操作"); + } + // 封装 + MailAccount account = new MailAccount(); + // 设置用户 + String user = emailConfig.getFromUser().split("@")[0]; + account.setUser(user); + account.setHost(emailConfig.getHost()); + account.setPort(Integer.parseInt(emailConfig.getPort())); + account.setAuth(true); + try { + // 对称解密 + account.setPass(EncryptUtils.desDecrypt(emailConfig.getPass())); + } catch (Exception e) { + throw new BadRequestException(e.getMessage()); + } + account.setFrom(emailConfig.getUser()+"<"+emailConfig.getFromUser()+">"); + // ssl方式发送 + account.setSslEnable(true); + // 使用STARTTLS安全连接 + account.setStarttlsEnable(true); + // 解决jdk8之后默认禁用部分tls协议,导致邮件发送失败的问题 + account.setSslProtocols("TLSv1 TLSv1.1 TLSv1.2"); + String content = emailVo.getContent(); + // 发送 + try { + int size = emailVo.getTos().size(); + Mail.create(account) + .setTos(emailVo.getTos().toArray(new String[size])) + .setTitle(emailVo.getSubject()) + .setContent(content) + .setHtml(true) + //关闭session + .setUseGlobalSession(false) + .send(); + }catch (Exception e){ + throw new BadRequestException(e.getMessage()); + } + } +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/service/impl/LocalStorageServiceImpl.java b/eladmin-tools/src/main/java/me/zhengjie/service/impl/LocalStorageServiceImpl.java new file mode 100644 index 0000000..9454853 --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/service/impl/LocalStorageServiceImpl.java @@ -0,0 +1,133 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import lombok.RequiredArgsConstructor; +import me.zhengjie.config.FileProperties; +import me.zhengjie.domain.LocalStorage; +import me.zhengjie.service.dto.LocalStorageDto; +import me.zhengjie.service.dto.LocalStorageQueryCriteria; +import me.zhengjie.service.mapstruct.LocalStorageMapper; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.utils.*; +import me.zhengjie.repository.LocalStorageRepository; +import me.zhengjie.service.LocalStorageService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletResponse; + +/** +* @author Zheng Jie +* @date 2019-09-05 +*/ +@Service +@RequiredArgsConstructor +public class LocalStorageServiceImpl implements LocalStorageService { + + private final LocalStorageRepository localStorageRepository; + private final LocalStorageMapper localStorageMapper; + private final FileProperties properties; + + @Override + public PageResult queryAll(LocalStorageQueryCriteria criteria, Pageable pageable){ + Page page = localStorageRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder),pageable); + return PageUtil.toPage(page.map(localStorageMapper::toDto)); + } + + @Override + public List queryAll(LocalStorageQueryCriteria criteria){ + return localStorageMapper.toDto(localStorageRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder))); + } + + @Override + public LocalStorageDto findById(Long id){ + LocalStorage localStorage = localStorageRepository.findById(id).orElseGet(LocalStorage::new); + ValidationUtil.isNull(localStorage.getId(),"LocalStorage","id",id); + return localStorageMapper.toDto(localStorage); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public LocalStorage create(String name, MultipartFile multipartFile) { + FileUtil.checkSize(properties.getMaxSize(), multipartFile.getSize()); + String suffix = FileUtil.getExtensionName(multipartFile.getOriginalFilename()); + String type = FileUtil.getFileType(suffix); + File file = FileUtil.upload(multipartFile, properties.getPath().getPath() + type + File.separator); + if(ObjectUtil.isNull(file)){ + throw new BadRequestException("上传失败"); + } + try { + name = StringUtils.isBlank(name) ? FileUtil.getFileNameNoEx(multipartFile.getOriginalFilename()) : name; + LocalStorage localStorage = new LocalStorage( + file.getName(), + name, + suffix, + file.getPath(), + type, + FileUtil.getSize(multipartFile.getSize()) + ); + return localStorageRepository.save(localStorage); + }catch (Exception e){ + FileUtil.del(file); + throw e; + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(LocalStorage resources) { + LocalStorage localStorage = localStorageRepository.findById(resources.getId()).orElseGet(LocalStorage::new); + ValidationUtil.isNull( localStorage.getId(),"LocalStorage","id",resources.getId()); + localStorage.copy(resources); + localStorageRepository.save(localStorage); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteAll(Long[] ids) { + for (Long id : ids) { + LocalStorage storage = localStorageRepository.findById(id).orElseGet(LocalStorage::new); + FileUtil.del(storage.getPath()); + localStorageRepository.delete(storage); + } + } + + @Override + public void download(List queryAll, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (LocalStorageDto localStorageDTO : queryAll) { + Map map = new LinkedHashMap<>(); + map.put("文件名", localStorageDTO.getRealName()); + map.put("备注名", localStorageDTO.getName()); + map.put("文件类型", localStorageDTO.getType()); + map.put("文件大小", localStorageDTO.getSize()); + map.put("创建者", localStorageDTO.getCreateBy()); + map.put("创建日期", localStorageDTO.getCreateTime()); + list.add(map); + } + FileUtil.downloadExcel(list, response); + } +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/service/impl/QiNiuServiceImpl.java b/eladmin-tools/src/main/java/me/zhengjie/service/impl/QiNiuServiceImpl.java new file mode 100644 index 0000000..60093d7 --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/service/impl/QiNiuServiceImpl.java @@ -0,0 +1,234 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service.impl; + +import com.alibaba.fastjson.JSON; +import com.qiniu.common.QiniuException; +import com.qiniu.http.Response; +import com.qiniu.storage.BucketManager; +import com.qiniu.storage.Configuration; +import com.qiniu.storage.UploadManager; +import com.qiniu.storage.model.DefaultPutRet; +import com.qiniu.storage.model.FileInfo; +import com.qiniu.util.Auth; +import lombok.RequiredArgsConstructor; +import me.zhengjie.domain.QiniuConfig; +import me.zhengjie.domain.QiniuContent; +import me.zhengjie.repository.QiniuContentRepository; +import me.zhengjie.service.dto.QiniuQueryCriteria; +import me.zhengjie.utils.*; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.repository.QiNiuConfigRepository; +import me.zhengjie.service.QiNiuService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; + +/** + * @author Zheng Jie + * @date 2018-12-31 + */ +@Service +@RequiredArgsConstructor +@CacheConfig(cacheNames = "qiNiu") +public class QiNiuServiceImpl implements QiNiuService { + + private final QiNiuConfigRepository qiNiuConfigRepository; + private final QiniuContentRepository qiniuContentRepository; + + @Value("${qiniu.max-size}") + private Long maxSize; + + @Override + @Cacheable(key = "'config'") + public QiniuConfig find() { + Optional qiniuConfig = qiNiuConfigRepository.findById(1L); + return qiniuConfig.orElseGet(QiniuConfig::new); + } + + @Override + @CachePut(key = "'config'") + @Transactional(rollbackFor = Exception.class) + public QiniuConfig config(QiniuConfig qiniuConfig) { + qiniuConfig.setId(1L); + String http = "http://", https = "https://"; + if (!(qiniuConfig.getHost().toLowerCase().startsWith(http)||qiniuConfig.getHost().toLowerCase().startsWith(https))) { + throw new BadRequestException("外链域名必须以http://或者https://开头"); + } + return qiNiuConfigRepository.save(qiniuConfig); + } + + @Override + public PageResult queryAll(QiniuQueryCriteria criteria, Pageable pageable){ + return PageUtil.toPage(qiniuContentRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder),pageable)); + } + + @Override + public List queryAll(QiniuQueryCriteria criteria) { + return qiniuContentRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root,criteria,criteriaBuilder)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public QiniuContent upload(MultipartFile file, QiniuConfig qiniuConfig) { + FileUtil.checkSize(maxSize, file.getSize()); + if(qiniuConfig.getId() == null){ + throw new BadRequestException("请先添加相应配置,再操作"); + } + // 构造一个带指定Zone对象的配置类 + Configuration cfg = new Configuration(QiNiuUtil.getRegion(qiniuConfig.getZone())); + UploadManager uploadManager = new UploadManager(cfg); + Auth auth = Auth.create(qiniuConfig.getAccessKey(), qiniuConfig.getSecretKey()); + String upToken = auth.uploadToken(qiniuConfig.getBucket()); + try { + String key = file.getOriginalFilename(); + if(qiniuContentRepository.findByKey(key) != null) { + key = QiNiuUtil.getKey(key); + } + Response response = uploadManager.put(file.getBytes(), key, upToken); + //解析上传成功的结果 + + DefaultPutRet putRet = JSON.parseObject(response.bodyString(), DefaultPutRet.class); + QiniuContent content = qiniuContentRepository.findByKey(FileUtil.getFileNameNoEx(putRet.key)); + if(content == null){ + //存入数据库 + QiniuContent qiniuContent = new QiniuContent(); + qiniuContent.setSuffix(FileUtil.getExtensionName(putRet.key)); + qiniuContent.setBucket(qiniuConfig.getBucket()); + qiniuContent.setType(qiniuConfig.getType()); + qiniuContent.setKey(FileUtil.getFileNameNoEx(putRet.key)); + qiniuContent.setUrl(qiniuConfig.getHost()+"/"+putRet.key); + qiniuContent.setSize(FileUtil.getSize(Integer.parseInt(String.valueOf(file.getSize())))); + return qiniuContentRepository.save(qiniuContent); + } + return content; + } catch (Exception e) { + throw new BadRequestException(e.getMessage()); + } + } + + @Override + public QiniuContent findByContentId(Long id) { + QiniuContent qiniuContent = qiniuContentRepository.findById(id).orElseGet(QiniuContent::new); + ValidationUtil.isNull(qiniuContent.getId(),"QiniuContent", "id",id); + return qiniuContent; + } + + @Override + public String download(QiniuContent content,QiniuConfig config){ + String finalUrl; + String type = "公开"; + if(type.equals(content.getType())){ + finalUrl = content.getUrl(); + } else { + Auth auth = Auth.create(config.getAccessKey(), config.getSecretKey()); + // 1小时,可以自定义链接过期时间 + long expireInSeconds = 3600; + finalUrl = auth.privateDownloadUrl(content.getUrl(), expireInSeconds); + } + return finalUrl; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(QiniuContent content, QiniuConfig config) { + //构造一个带指定Zone对象的配置类 + Configuration cfg = new Configuration(QiNiuUtil.getRegion(config.getZone())); + Auth auth = Auth.create(config.getAccessKey(), config.getSecretKey()); + BucketManager bucketManager = new BucketManager(auth, cfg); + try { + bucketManager.delete(content.getBucket(), content.getKey() + "." + content.getSuffix()); + qiniuContentRepository.delete(content); + } catch (QiniuException ex) { + qiniuContentRepository.delete(content); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void synchronize(QiniuConfig config) { + if(config.getId() == null){ + throw new BadRequestException("请先添加相应配置,再操作"); + } + //构造一个带指定Zone对象的配置类 + Configuration cfg = new Configuration(QiNiuUtil.getRegion(config.getZone())); + Auth auth = Auth.create(config.getAccessKey(), config.getSecretKey()); + BucketManager bucketManager = new BucketManager(auth, cfg); + //文件名前缀 + String prefix = ""; + //每次迭代的长度限制,最大1000,推荐值 1000 + int limit = 1000; + //指定目录分隔符,列出所有公共前缀(模拟列出目录效果)。缺省值为空字符串 + String delimiter = ""; + //列举空间文件列表 + BucketManager.FileListIterator fileListIterator = bucketManager.createFileListIterator(config.getBucket(), prefix, limit, delimiter); + while (fileListIterator.hasNext()) { + //处理获取的file list结果 + QiniuContent qiniuContent; + FileInfo[] items = fileListIterator.next(); + for (FileInfo item : items) { + if(qiniuContentRepository.findByKey(FileUtil.getFileNameNoEx(item.key)) == null){ + qiniuContent = new QiniuContent(); + qiniuContent.setSize(FileUtil.getSize(Integer.parseInt(String.valueOf(item.fsize)))); + qiniuContent.setSuffix(FileUtil.getExtensionName(item.key)); + qiniuContent.setKey(FileUtil.getFileNameNoEx(item.key)); + qiniuContent.setType(config.getType()); + qiniuContent.setBucket(config.getBucket()); + qiniuContent.setUrl(config.getHost()+"/"+item.key); + qiniuContentRepository.save(qiniuContent); + } + } + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteAll(Long[] ids, QiniuConfig config) { + for (Long id : ids) { + delete(findByContentId(id), config); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(String type) { + qiNiuConfigRepository.update(type); + } + + @Override + public void downloadList(List queryAll, HttpServletResponse response) throws IOException { + List> list = new ArrayList<>(); + for (QiniuContent content : queryAll) { + Map map = new LinkedHashMap<>(); + map.put("文件名", content.getKey()); + map.put("文件类型", content.getSuffix()); + map.put("空间名称", content.getBucket()); + map.put("文件大小", content.getSize()); + map.put("空间类型", content.getType()); + map.put("创建日期", content.getUpdateTime()); + list.add(map); + } + FileUtil.downloadExcel(list, response); + } +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/service/mapstruct/LocalStorageMapper.java b/eladmin-tools/src/main/java/me/zhengjie/service/mapstruct/LocalStorageMapper.java new file mode 100644 index 0000000..4c6d955 --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/service/mapstruct/LocalStorageMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.service.mapstruct; + +import me.zhengjie.base.BaseMapper; +import me.zhengjie.service.dto.LocalStorageDto; +import me.zhengjie.domain.LocalStorage; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +/** +* @author Zheng Jie +* @date 2019-09-05 +*/ +@Mapper(componentModel = "spring",unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface LocalStorageMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/eladmin-tools/src/main/java/me/zhengjie/utils/AliPayStatusEnum.java b/eladmin-tools/src/main/java/me/zhengjie/utils/AliPayStatusEnum.java new file mode 100644 index 0000000..c114fd1 --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/utils/AliPayStatusEnum.java @@ -0,0 +1,46 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.utils; + +/** + * 支付状态 + * @author zhengjie + * @date 2018/08/01 16:45:43 + */ +public enum AliPayStatusEnum { + + /** 交易成功 */ + FINISHED("TRADE_FINISHED"), + + /** 支付成功 */ + SUCCESS("TRADE_SUCCESS"), + + /** 交易创建 */ + BUYER_PAY("WAIT_BUYER_PAY"), + + /** 交易关闭 */ + CLOSED("TRADE_CLOSED"); + + private final String value; + + AliPayStatusEnum(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/utils/AlipayUtils.java b/eladmin-tools/src/main/java/me/zhengjie/utils/AlipayUtils.java new file mode 100644 index 0000000..203f7db --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/utils/AlipayUtils.java @@ -0,0 +1,85 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.utils; + +import com.alipay.api.AlipayApiException; +import com.alipay.api.internal.util.AlipaySignature; +import me.zhengjie.domain.AlipayConfig; +import org.springframework.stereotype.Component; +import javax.servlet.http.HttpServletRequest; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * 支付宝工具类 + * @author zhengjie + * @date 2018/09/30 14:04:35 + */ +@Component +public class AlipayUtils { + + /** + * 生成订单号 + * @return String + */ + public String getOrderCode() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + int a = (int)(Math.random() * 9000.0D) + 1000; + System.out.println(a); + Date date = new Date(); + String str = sdf.format(date); + String[] split = str.split("-"); + String s = split[0] + split[1] + split[2]; + String[] split1 = s.split(" "); + String s1 = split1[0] + split1[1]; + String[] split2 = s1.split(":"); + return split2[0] + split2[1] + split2[2] + a; + } + + /** + * 校验签名 + * @param request HttpServletRequest + * @param alipay 阿里云配置 + * @return boolean + */ + public boolean rsaCheck(HttpServletRequest request, AlipayConfig alipay){ + + // 获取支付宝POST过来反馈信息 + Map params = new HashMap<>(1); + Map requestParams = request.getParameterMap(); + for (Object o : requestParams.keySet()) { + String name = (String) o; + String[] values = requestParams.get(name); + String valueStr = ""; + for (int i = 0; i < values.length; i++) { + valueStr = (i == values.length - 1) ? valueStr + values[i] + : valueStr + values[i] + ","; + } + params.put(name, valueStr); + } + + try { + return AlipaySignature.rsaCheckV1(params, + alipay.getPublicKey(), + alipay.getCharset(), + alipay.getSignType()); + } catch (AlipayApiException e) { + return false; + } + } +} diff --git a/eladmin-tools/src/main/java/me/zhengjie/utils/QiNiuUtil.java b/eladmin-tools/src/main/java/me/zhengjie/utils/QiNiuUtil.java new file mode 100644 index 0000000..8091a95 --- /dev/null +++ b/eladmin-tools/src/main/java/me/zhengjie/utils/QiNiuUtil.java @@ -0,0 +1,71 @@ +/* + * Copyright 2019-2020 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.zhengjie.utils; + +import com.qiniu.storage.Region; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * 七牛云存储工具类 + * @author Zheng Jie + * @date 2018-12-31 + */ +public class QiNiuUtil { + + private static final String HUAD = "华东"; + + private static final String HUAB = "华北"; + + private static final String HUAN = "华南"; + + private static final String BEIM = "北美"; + + /** + * 得到机房的对应关系 + * @param zone 机房名称 + * @return Region + */ + public static Region getRegion(String zone){ + + if(HUAD.equals(zone)){ + return Region.huadong(); + } else if(HUAB.equals(zone)){ + return Region.huabei(); + } else if(HUAN.equals(zone)){ + return Region.huanan(); + } else if (BEIM.equals(zone)){ + return Region.beimei(); + // 否则就是东南亚 + } else { + return Region.qvmHuadong(); + } + } + + /** + * 默认不指定key的情况下,以文件内容的hash值作为文件名 + * @param file 文件名 + * @return String + */ + public static String getKey(String file){ + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); + Date date = new Date(); + return FileUtil.getFileNameNoEx(file) + "-" + + sdf.format(date) + + "." + + FileUtil.getExtensionName(file); + } +} diff --git a/eladmin-web/.editorconfig b/eladmin-web/.editorconfig new file mode 100644 index 0000000..3454886 --- /dev/null +++ b/eladmin-web/.editorconfig @@ -0,0 +1,14 @@ +# https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/eladmin-web/.env.development b/eladmin-web/.env.development new file mode 100644 index 0000000..d9a2132 --- /dev/null +++ b/eladmin-web/.env.development @@ -0,0 +1,8 @@ +ENV = 'development' + +# 接口地址 +VUE_APP_BASE_API = 'http://localhost:8000' +VUE_APP_WS_API = 'ws://localhost:8000' + +# 是否启用 babel-plugin-dynamic-import-node插件 +VUE_CLI_BABEL_TRANSPILE_MODULES = true diff --git a/eladmin-web/.env.production b/eladmin-web/.env.production new file mode 100644 index 0000000..66da123 --- /dev/null +++ b/eladmin-web/.env.production @@ -0,0 +1,7 @@ +ENV = 'production' + +# 如果使用 Nginx 代理后端接口,那么此处需要改为 '/',文件查看 Docker 部署篇,Nginx 配置 +# 接口地址,注意协议,如果你没有配置 ssl,需要将 https 改为 http +VUE_APP_BASE_API = 'https://eladmin.vip' +# 如果接口是 http 形式, wss 需要改为 ws +VUE_APP_WS_API = 'wss://eladmin.vip' diff --git a/eladmin-web/.eslintignore b/eladmin-web/.eslintignore new file mode 100644 index 0000000..e6529fc --- /dev/null +++ b/eladmin-web/.eslintignore @@ -0,0 +1,4 @@ +build/*.js +src/assets +public +dist diff --git a/eladmin-web/.eslintrc.js b/eladmin-web/.eslintrc.js new file mode 100644 index 0000000..c977505 --- /dev/null +++ b/eladmin-web/.eslintrc.js @@ -0,0 +1,198 @@ +module.exports = { + root: true, + parserOptions: { + parser: 'babel-eslint', + sourceType: 'module' + }, + env: { + browser: true, + node: true, + es6: true, + }, + extends: ['plugin:vue/recommended', 'eslint:recommended'], + + // add your custom rules here + //it is base on https://github.com/vuejs/eslint-config-vue + rules: { + "vue/max-attributes-per-line": [2, { + "singleline": 10, + "multiline": { + "max": 1, + "allowFirstLine": false + } + }], + "vue/singleline-html-element-content-newline": "off", + "vue/multiline-html-element-content-newline":"off", + "vue/name-property-casing": ["error", "PascalCase"], + "vue/no-v-html": "off", + 'accessor-pairs': 2, + 'arrow-spacing': [2, { + 'before': true, + 'after': true + }], + 'block-spacing': [2, 'always'], + 'brace-style': [2, '1tbs', { + 'allowSingleLine': true + }], + 'camelcase': [0, { + 'properties': 'always' + }], + 'comma-dangle': [2, 'never'], + 'comma-spacing': [2, { + 'before': false, + 'after': true + }], + 'comma-style': [2, 'last'], + 'constructor-super': 2, + 'curly': [2, 'multi-line'], + 'dot-location': [2, 'property'], + 'eol-last': 2, + 'eqeqeq': ["error", "always", {"null": "ignore"}], + 'generator-star-spacing': [2, { + 'before': true, + 'after': true + }], + 'handle-callback-err': [2, '^(err|error)$'], + 'indent': [2, 2, { + 'SwitchCase': 1 + }], + 'jsx-quotes': [2, 'prefer-single'], + 'key-spacing': [2, { + 'beforeColon': false, + 'afterColon': true + }], + 'keyword-spacing': [2, { + 'before': true, + 'after': true + }], + 'new-cap': [2, { + 'newIsCap': true, + 'capIsNew': false + }], + 'new-parens': 2, + 'no-array-constructor': 2, + 'no-caller': 2, + 'no-console': 'off', + 'no-class-assign': 2, + 'no-cond-assign': 2, + 'no-const-assign': 2, + 'no-control-regex': 0, + 'no-delete-var': 2, + 'no-dupe-args': 2, + 'no-dupe-class-members': 2, + 'no-dupe-keys': 2, + 'no-duplicate-case': 2, + 'no-empty-character-class': 2, + 'no-empty-pattern': 2, + 'no-eval': 2, + 'no-ex-assign': 2, + 'no-extend-native': 2, + 'no-extra-bind': 2, + 'no-extra-boolean-cast': 2, + 'no-extra-parens': [2, 'functions'], + 'no-fallthrough': 2, + 'no-floating-decimal': 2, + 'no-func-assign': 2, + 'no-implied-eval': 2, + 'no-inner-declarations': [2, 'functions'], + 'no-invalid-regexp': 2, + 'no-irregular-whitespace': 2, + 'no-iterator': 2, + 'no-label-var': 2, + 'no-labels': [2, { + 'allowLoop': false, + 'allowSwitch': false + }], + 'no-lone-blocks': 2, + 'no-mixed-spaces-and-tabs': 2, + 'no-multi-spaces': 2, + 'no-multi-str': 2, + 'no-multiple-empty-lines': [2, { + 'max': 1 + }], + 'no-native-reassign': 2, + 'no-negated-in-lhs': 2, + 'no-new-object': 2, + 'no-new-require': 2, + 'no-new-symbol': 2, + 'no-new-wrappers': 2, + 'no-obj-calls': 2, + 'no-octal': 2, + 'no-octal-escape': 2, + 'no-path-concat': 2, + 'no-proto': 2, + 'no-redeclare': 2, + 'no-regex-spaces': 2, + 'no-return-assign': [2, 'except-parens'], + 'no-self-assign': 2, + 'no-self-compare': 2, + 'no-sequences': 2, + 'no-shadow-restricted-names': 2, + 'no-spaced-func': 2, + 'no-sparse-arrays': 2, + 'no-this-before-super': 2, + 'no-throw-literal': 2, + 'no-trailing-spaces': 2, + 'no-undef': 2, + 'no-undef-init': 2, + 'no-unexpected-multiline': 2, + 'no-unmodified-loop-condition': 2, + 'no-unneeded-ternary': [2, { + 'defaultAssignment': false + }], + 'no-unreachable': 2, + 'no-unsafe-finally': 2, + 'no-unused-vars': [2, { + 'vars': 'all', + 'args': 'none' + }], + 'no-useless-call': 2, + 'no-useless-computed-key': 2, + 'no-useless-constructor': 2, + 'no-useless-escape': 0, + 'no-whitespace-before-property': 2, + 'no-with': 2, + 'one-var': [2, { + 'initialized': 'never' + }], + 'operator-linebreak': [2, 'after', { + 'overrides': { + '?': 'before', + ':': 'before' + } + }], + 'padded-blocks': [2, 'never'], + 'quotes': [2, 'single', { + 'avoidEscape': true, + 'allowTemplateLiterals': true + }], + 'semi': [2, 'never'], + 'semi-spacing': [2, { + 'before': false, + 'after': true + }], + 'space-before-blocks': [2, 'always'], + 'space-before-function-paren': [2, 'never'], + 'space-in-parens': [2, 'never'], + 'space-infix-ops': 2, + 'space-unary-ops': [2, { + 'words': true, + 'nonwords': false + }], + 'spaced-comment': [2, 'always', { + 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] + }], + 'template-curly-spacing': [2, 'never'], + 'use-isnan': 2, + 'valid-typeof': 2, + 'wrap-iife': [2, 'any'], + 'yield-star-spacing': [2, 'both'], + 'yoda': [2, 'never'], + 'prefer-const': 2, + 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, + 'object-curly-spacing': [2, 'always', { + objectsInObjects: false + }], + 'array-bracket-spacing': [2, 'never'] + } +} diff --git a/eladmin-web/.gitignore b/eladmin-web/.gitignore new file mode 100644 index 0000000..1978bc2 --- /dev/null +++ b/eladmin-web/.gitignore @@ -0,0 +1,24 @@ +.DS_Store +node_modules/ +dist/ +demo/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +**/*.log + +tests/**/coverage/ +tests/e2e/reports +selenium-debug.log + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.local + +package-lock.json +yarn.lock diff --git a/eladmin-web/.travis.yml b/eladmin-web/.travis.yml new file mode 100644 index 0000000..f4be7a0 --- /dev/null +++ b/eladmin-web/.travis.yml @@ -0,0 +1,5 @@ +language: node_js +node_js: 10 +script: npm run test +notifications: + email: false diff --git a/eladmin-web/LICENSE b/eladmin-web/LICENSE new file mode 100644 index 0000000..78a36cb --- /dev/null +++ b/eladmin-web/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "{}" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright 2019 Zheng Jie + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/eladmin-web/README.md b/eladmin-web/README.md new file mode 100644 index 0000000..71d32bf --- /dev/null +++ b/eladmin-web/README.md @@ -0,0 +1,77 @@ +# ELADMIN-WEB + +ELADMIN 前端源码 + +#### 项目源码 + +| | 后端源码 | 前端源码 | +|--- |--- | --- | +| github | https://github.com/elunez/eladmin | https://github.com/elunez/eladmin-web | +| 码云 | https://gitee.com/elunez/eladmin | https://gitee.com/elunez/eladmin-web | + +#### 开发文档 +[https://eladmin.vip](https://eladmin.vip) + +#### 体验地址 +[https://eladmin.vip/demo](https://eladmin.vip/demo) + +#### 前端模板 + +初始模板基于: [https://github.com/PanJiaChen/vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) + +模板文档: [https://panjiachen.github.io/vue-element-admin-site/zh/guide/](https://panjiachen.github.io/vue-element-admin-site/zh/guide/) + +#### VPS推荐 + + + + +使用优惠码: `BWHNCXNVXV`,可获得 6.81% 的折扣, [查看介绍](https://eladmin.vip/pages/040101/) + +#### Build Setup +**推荐 node 版本:12-16** +``` bash +# 配置镜像加速 +https://www.ydyno.com/archives/1219.html + +# 安装依赖 +npm install + +# 启动服务 localhost:8013 +npm run dev + +# 构建生产环境 +npm run build:prod +``` + +#### 常见问题 + +1、linux 系统在安装依赖的时候会出现 node-sass 无法安装的问题 + +解决方案: +``` +1. 单独安装:npm install --unsafe-perm node-sass +2. 直接使用:npm install --unsafe-perm +``` + +2、加速node-sass安装 + +https://www.ydyno.com/archives/1219.html + +#### 特别鸣谢 + +- 感谢 [JetBrains](https://www.jetbrains.com/) 提供的非商业开源软件开发授权 + +- 感谢 [PanJiaChen](https://github.com/PanJiaChen/vue-element-admin) 大佬提供的前端模板 + +- 感谢 [Moxun](https://github.com/moxun1639) 大佬提供的前端 Crud 通用组件 + +- 感谢 [zhy6599](https://gitee.com/zhy6599) 大佬提供的后端运维管理相关功能 + +- 感谢 [j.yao.SUSE](https://github.com/everhopingandwaiting) 大佬提供的匿名接口与Redis限流等功能 + +- 感谢 [d15801543974](https://github.com/d15801543974) 大佬提供的基于注解的通用查询方式 + +#### 反馈交流 + +- QQ交流群:891137268 、947578238、659622532 \ No newline at end of file diff --git a/eladmin-web/babel.config.js b/eladmin-web/babel.config.js new file mode 100644 index 0000000..804632a --- /dev/null +++ b/eladmin-web/babel.config.js @@ -0,0 +1,11 @@ +const plugins = ['@vue/babel-plugin-transform-vue-jsx'] +// 生产环境移除console +if (process.env.NODE_ENV === 'production') { + plugins.push('transform-remove-console') +} +module.exports = { + plugins: plugins, + presets: [ + '@vue/app' + ] +} diff --git a/eladmin-web/jest.config.js b/eladmin-web/jest.config.js new file mode 100644 index 0000000..143cdc8 --- /dev/null +++ b/eladmin-web/jest.config.js @@ -0,0 +1,24 @@ +module.exports = { + moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], + transform: { + '^.+\\.vue$': 'vue-jest', + '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': + 'jest-transform-stub', + '^.+\\.jsx?$': 'babel-jest' + }, + moduleNameMapper: { + '^@/(.*)$': '/src/$1' + }, + snapshotSerializers: ['jest-serializer-vue'], + testMatch: [ + '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' + ], + collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], + coverageDirectory: '/tests/unit/coverage', + // 'collectCoverage': true, + 'coverageReporters': [ + 'lcov', + 'text-summary' + ], + testURL: 'http://localhost/' +} diff --git a/eladmin-web/package.json b/eladmin-web/package.json new file mode 100644 index 0000000..689e255 --- /dev/null +++ b/eladmin-web/package.json @@ -0,0 +1,115 @@ +{ + "name": "eladmin-web", + "version": "2.7.0", + "description": "ELADMIN 前端源码", + "author": "Zheng Jie", + "license": "Apache-2.0", + "scripts": { + "dev": "vue-cli-service serve", + "build:prod": "vue-cli-service build", + "build:stage": "vue-cli-service build --mode staging", + "preview": "node build/index.js --preview", + "lint": "eslint --ext .js,.vue src", + "test:unit": "jest --clearCache && vue-cli-service test:unit", + "svgo": "svgo -f src/assets/icons/svg --config=src/assets/icons/svgo.yml", + "new": "plop" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "src/**/*.{js,vue}": [ + "eslint --fix", + "git add" + ] + }, + "repository": { + "type": "git", + "url": "https://github.com/elunez/eladmin-web.git" + }, + "bugs": { + "url": "https://github.com/elunez/eladmin/issues" + }, + "dependencies": { + "@riophae/vue-treeselect": "^0.4.0", + "@wangeditor/editor": "^5.1.23", + "@wangeditor/editor-for-vue": "^1.0.2", + "axios": "^0.21.1", + "clipboard": "2.0.4", + "codemirror": "^5.49.2", + "core-js": "^2.6.12", + "echarts": "^4.2.1", + "echarts-wordcloud": "^1.1.3", + "element-ui": "^2.15.8", + "file-saver": "1.3.8", + "fuse.js": "3.4.4", + "js-beautify": "^1.10.2", + "js-cookie": "2.2.0", + "jsencrypt": "^3.0.0-rc.1", + "jszip": "^3.7.1", + "mavon-editor": "^2.9.1", + "normalize.css": "7.0.0", + "nprogress": "0.2.0", + "path-to-regexp": "2.4.0", + "qs": "^6.10.1", + "screenfull": "4.2.0", + "sortablejs": "1.8.4", + "vue": "^2.6.14", + "vue-count-to": "^1.0.13", + "vue-cropper": "0.4.9", + "vue-echarts": "^5.0.0-beta.0", + "vue-image-crop-upload": "^2.5.0", + "vue-router": "3.0.2", + "vue-splitpane": "1.0.4", + "vuedraggable": "2.20.0", + "vuex": "3.1.0", + "wangeditor": "^4.7.11", + "webpack": "^4.46.0" + }, + "devDependencies": { + "@babel/parser": "^7.7.4", + "@babel/register": "7.0.0", + "@vue/babel-plugin-transform-vue-jsx": "^1.2.1", + "@vue/cli-plugin-babel": "3.5.3", + "@vue/cli-plugin-eslint": "^3.9.1", + "@vue/cli-plugin-unit-jest": "3.5.3", + "@vue/cli-service": "3.5.3", + "@vue/test-utils": "1.0.0-beta.29", + "autoprefixer": "^9.5.1", + "babel-core": "7.0.0-bridge.0", + "babel-eslint": "10.0.1", + "babel-jest": "23.6.0", + "babel-plugin-dynamic-import-node": "2.3.0", + "babel-plugin-transform-remove-console": "^6.9.4", + "chalk": "2.4.2", + "chokidar": "2.1.5", + "compression-webpack-plugin": "5.0.2", + "connect": "3.6.6", + "eslint": "5.15.3", + "eslint-plugin-vue": "5.2.2", + "html-webpack-plugin": "3.2.0", + "http-proxy-middleware": "^0.19.1", + "husky": "1.3.1", + "lint-staged": "8.1.5", + "plop": "2.3.0", + "sass": "1.32.13", + "sass-loader": "10.2.0", + "script-ext-html-webpack-plugin": "2.1.3", + "script-loader": "0.7.2", + "serve-static": "^1.13.2", + "svg-sprite-loader": "4.1.3", + "svgo": "1.2.0", + "tasksfile": "^5.1.1", + "vue-template-compiler": "2.6.14" + }, + "engines": { + "node": ">=8.9", + "npm": ">= 3.0.0" + }, + "browserslist": [ + "> 1%", + "last 2 versions" + ] +} diff --git a/eladmin-web/plopfile.js b/eladmin-web/plopfile.js new file mode 100644 index 0000000..9f3147e --- /dev/null +++ b/eladmin-web/plopfile.js @@ -0,0 +1,7 @@ +const viewGenerator = require('./plop-templates/view/prompt') +const componentGenerator = require('./plop-templates/component/prompt') + +module.exports = function(plop) { + plop.setGenerator('view', viewGenerator) + plop.setGenerator('component', componentGenerator) +} diff --git a/eladmin-web/postcss.config.js b/eladmin-web/postcss.config.js new file mode 100644 index 0000000..961986e --- /dev/null +++ b/eladmin-web/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: { + autoprefixer: {} + } +} diff --git a/eladmin-web/public/favicon.ico b/eladmin-web/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..fcb69992e5f0777989b89db9a6318b3621eff240 GIT binary patch literal 67646 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 U$^5!t50tOVCyZ47oXe;G53^4nod5s; literal 0 HcmV?d00001 diff --git a/eladmin-web/public/index.html b/eladmin-web/public/index.html new file mode 100644 index 0000000..e918500 --- /dev/null +++ b/eladmin-web/public/index.html @@ -0,0 +1,15 @@ + + + + + + + + + <%= webpackConfig.name %> + + +
+ + + diff --git a/eladmin-web/public/robots.txt b/eladmin-web/public/robots.txt new file mode 100644 index 0000000..77470cb --- /dev/null +++ b/eladmin-web/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/eladmin-web/src/App.vue b/eladmin-web/src/App.vue new file mode 100644 index 0000000..ec9032c --- /dev/null +++ b/eladmin-web/src/App.vue @@ -0,0 +1,11 @@ + + + diff --git a/eladmin-web/src/api/data.js b/eladmin-web/src/api/data.js new file mode 100644 index 0000000..d975d75 --- /dev/null +++ b/eladmin-web/src/api/data.js @@ -0,0 +1,17 @@ +import request from '@/utils/request' +import qs from 'qs' + +export function initData(url, params) { + return request({ + url: url + '?' + qs.stringify(params, { indices: false }), + method: 'get' + }) +} + +export function download(url, params) { + return request({ + url: url + '?' + qs.stringify(params, { indices: false }), + method: 'get', + responseType: 'blob' + }) +} diff --git a/eladmin-web/src/api/generator/genConfig.js b/eladmin-web/src/api/generator/genConfig.js new file mode 100644 index 0000000..e15b200 --- /dev/null +++ b/eladmin-web/src/api/generator/genConfig.js @@ -0,0 +1,16 @@ +import request from '@/utils/request' + +export function get(tableName) { + return request({ + url: 'api/genConfig/' + tableName, + method: 'get' + }) +} + +export function update(data) { + return request({ + url: 'api/genConfig', + data, + method: 'put' + }) +} diff --git a/eladmin-web/src/api/generator/generator.js b/eladmin-web/src/api/generator/generator.js new file mode 100644 index 0000000..0c49718 --- /dev/null +++ b/eladmin-web/src/api/generator/generator.js @@ -0,0 +1,33 @@ +import request from '@/utils/request' + +export function getAllTable() { + return request({ + url: 'api/generator/tables/all', + method: 'get' + }) +} + +export function generator(tableName, type) { + return request({ + url: 'api/generator/' + tableName + '/' + type, + method: 'post', + responseType: type === 2 ? 'blob' : '' + }) +} + +export function save(data) { + return request({ + url: 'api/generator', + data, + method: 'put' + }) +} + +export function sync(tables) { + return request({ + url: 'api/generator/sync', + method: 'post', + data: tables + }) +} + diff --git a/eladmin-web/src/api/login.js b/eladmin-web/src/api/login.js new file mode 100644 index 0000000..fe257d2 --- /dev/null +++ b/eladmin-web/src/api/login.js @@ -0,0 +1,35 @@ +import request from '@/utils/request' + +export function login(username, password, code, uuid) { + return request({ + url: 'auth/login', + method: 'post', + data: { + username, + password, + code, + uuid + } + }) +} + +export function getInfo() { + return request({ + url: 'auth/info', + method: 'get' + }) +} + +export function getCodeImg() { + return request({ + url: 'auth/code', + method: 'get' + }) +} + +export function logout() { + return request({ + url: 'auth/logout', + method: 'delete' + }) +} diff --git a/eladmin-web/src/api/mnt/app.js b/eladmin-web/src/api/mnt/app.js new file mode 100644 index 0000000..2a27054 --- /dev/null +++ b/eladmin-web/src/api/mnt/app.js @@ -0,0 +1,27 @@ +import request from '@/utils/request' + +export function add(data) { + return request({ + url: 'api/app', + method: 'post', + data + }) +} + +export function del(ids) { + return request({ + url: 'api/app', + method: 'delete', + data: ids + }) +} + +export function edit(data) { + return request({ + url: 'api/app', + method: 'put', + data + }) +} + +export default { add, edit, del } diff --git a/eladmin-web/src/api/mnt/connect.js b/eladmin-web/src/api/mnt/connect.js new file mode 100644 index 0000000..1bbe90b --- /dev/null +++ b/eladmin-web/src/api/mnt/connect.js @@ -0,0 +1,17 @@ +import request from '@/utils/request' + +export function testDbConnect(data) { + return request({ + url: 'api/database/testConnect', + method: 'post', + data + }) +} + +export function testServerConnect(data) { + return request({ + url: 'api/serverDeploy/testConnect', + method: 'post', + data + }) +} diff --git a/eladmin-web/src/api/mnt/database.js b/eladmin-web/src/api/mnt/database.js new file mode 100644 index 0000000..91797fb --- /dev/null +++ b/eladmin-web/src/api/mnt/database.js @@ -0,0 +1,35 @@ +import request from '@/utils/request' + +export function add(data) { + return request({ + url: 'api/database', + method: 'post', + data + }) +} + +export function del(ids) { + return request({ + url: 'api/database', + method: 'delete', + data: ids + }) +} + +export function edit(data) { + return request({ + url: 'api/database', + method: 'put', + data + }) +} + +export function testDbConnection(data) { + return request({ + url: 'api/database/testConnect', + method: 'post', + data + }) +} + +export default { add, edit, del, testDbConnection } diff --git a/eladmin-web/src/api/mnt/deploy.js b/eladmin-web/src/api/mnt/deploy.js new file mode 100644 index 0000000..c1475ea --- /dev/null +++ b/eladmin-web/src/api/mnt/deploy.js @@ -0,0 +1,77 @@ +import request from '@/utils/request' + +export function add(data) { + return request({ + url: 'api/deploy', + method: 'post', + data + }) +} + +export function del(ids) { + return request({ + url: 'api/deploy', + method: 'delete', + data: ids + }) +} + +export function edit(data) { + return request({ + url: 'api/deploy', + method: 'put', + data + }) +} + +export function getApps() { + return request({ + url: 'api/app', + method: 'get' + }) +} + +export function getServers() { + return request({ + url: 'api/serverDeploy', + method: 'get' + }) +} + +/** + * 启动服务 + * @param data 选中行 + */ +export function startServer(data) { + return request({ + url: 'api/deploy/startServer', + method: 'post', + data + }) +} + +/** + * 停止服务 + * @param data 选中行 + */ +export function stopServer(data) { + return request({ + url: 'api/deploy/stopServer', + method: 'post', + data + }) +} + +/** + * 停止服务 + * @param data 选中行 + */ +export function serverStatus(data) { + return request({ + url: 'api/deploy/serverStatus', + method: 'post', + data + }) +} + +export default { add, edit, del, stopServer, serverStatus, startServer, getServers, getApps } diff --git a/eladmin-web/src/api/mnt/deployHistory.js b/eladmin-web/src/api/mnt/deployHistory.js new file mode 100644 index 0000000..30335e4 --- /dev/null +++ b/eladmin-web/src/api/mnt/deployHistory.js @@ -0,0 +1,21 @@ +import request from '@/utils/request' + +export function del(ids) { + return request({ + url: 'api/deployHistory', + method: 'delete', + data: ids + }) +} + +/** + * 版本回退 + * @param data 选中行 + */ +export function reducte(data) { + return request({ + url: 'api/deploy/serverReduction', + method: 'post', + data + }) +} diff --git a/eladmin-web/src/api/mnt/serverDeploy.js b/eladmin-web/src/api/mnt/serverDeploy.js new file mode 100644 index 0000000..e796114 --- /dev/null +++ b/eladmin-web/src/api/mnt/serverDeploy.js @@ -0,0 +1,27 @@ +import request from '@/utils/request' + +export function add(data) { + return request({ + url: 'api/serverDeploy', + method: 'post', + data + }) +} + +export function del(ids) { + return request({ + url: 'api/serverDeploy', + method: 'delete', + data: ids + }) +} + +export function edit(data) { + return request({ + url: 'api/serverDeploy', + method: 'put', + data + }) +} + +export default { add, edit, del } diff --git a/eladmin-web/src/api/monitor/log.js b/eladmin-web/src/api/monitor/log.js new file mode 100644 index 0000000..13f0d39 --- /dev/null +++ b/eladmin-web/src/api/monitor/log.js @@ -0,0 +1,22 @@ +import request from '@/utils/request' + +export function getErrDetail(id) { + return request({ + url: 'api/logs/error/' + id, + method: 'get' + }) +} + +export function delAllError() { + return request({ + url: 'api/logs/del/error', + method: 'delete' + }) +} + +export function delAllInfo() { + return request({ + url: 'api/logs/del/info', + method: 'delete' + }) +} diff --git a/eladmin-web/src/api/monitor/online.js b/eladmin-web/src/api/monitor/online.js new file mode 100644 index 0000000..057275b --- /dev/null +++ b/eladmin-web/src/api/monitor/online.js @@ -0,0 +1,9 @@ +import request from '@/utils/request' + +export function del(keys) { + return request({ + url: 'auth/online', + method: 'delete', + data: keys + }) +} diff --git a/eladmin-web/src/api/system/code.js b/eladmin-web/src/api/system/code.js new file mode 100644 index 0000000..0d2e4c2 --- /dev/null +++ b/eladmin-web/src/api/system/code.js @@ -0,0 +1,15 @@ +import request from '@/utils/request' + +export function resetEmail(data) { + return request({ + url: 'api/code/resetEmail?email=' + data, + method: 'post' + }) +} + +export function updatePass(pass) { + return request({ + url: 'api/users/updatePass/' + pass, + method: 'get' + }) +} diff --git a/eladmin-web/src/api/system/dept.js b/eladmin-web/src/api/system/dept.js new file mode 100644 index 0000000..8f920d0 --- /dev/null +++ b/eladmin-web/src/api/system/dept.js @@ -0,0 +1,45 @@ +import request from '@/utils/request' + +export function getDepts(params) { + return request({ + url: 'api/dept', + method: 'get', + params + }) +} + +export function getDeptSuperior(ids, exclude) { + exclude = exclude !== undefined ? exclude : false + const data = ids.length || ids.length === 0 ? ids : Array.of(ids) + return request({ + url: 'api/dept/superior?exclude=' + exclude, + method: 'post', + data + }) +} + +export function add(data) { + return request({ + url: 'api/dept', + method: 'post', + data + }) +} + +export function del(ids) { + return request({ + url: 'api/dept', + method: 'delete', + data: ids + }) +} + +export function edit(data) { + return request({ + url: 'api/dept', + method: 'put', + data + }) +} + +export default { add, edit, del, getDepts, getDeptSuperior } diff --git a/eladmin-web/src/api/system/dict.js b/eladmin-web/src/api/system/dict.js new file mode 100644 index 0000000..99170f7 --- /dev/null +++ b/eladmin-web/src/api/system/dict.js @@ -0,0 +1,34 @@ +import request from '@/utils/request' + +export function getDicts() { + return request({ + url: 'api/dict/all', + method: 'get' + }) +} + +export function add(data) { + return request({ + url: 'api/dict', + method: 'post', + data + }) +} + +export function del(ids) { + return request({ + url: 'api/dict/', + method: 'delete', + data: ids + }) +} + +export function edit(data) { + return request({ + url: 'api/dict', + method: 'put', + data + }) +} + +export default { add, edit, del } diff --git a/eladmin-web/src/api/system/dictDetail.js b/eladmin-web/src/api/system/dictDetail.js new file mode 100644 index 0000000..e8dc512 --- /dev/null +++ b/eladmin-web/src/api/system/dictDetail.js @@ -0,0 +1,52 @@ +import request from '@/utils/request' + +export function get(dictName) { + const params = { + dictName, + page: 0, + size: 9999 + } + return request({ + url: 'api/dictDetail', + method: 'get', + params + }) +} + +export function getDictMap(dictName) { + const params = { + dictName, + page: 0, + size: 9999 + } + return request({ + url: 'api/dictDetail/map', + method: 'get', + params + }) +} + +export function add(data) { + return request({ + url: 'api/dictDetail', + method: 'post', + data + }) +} + +export function del(id) { + return request({ + url: 'api/dictDetail/' + id, + method: 'delete' + }) +} + +export function edit(data) { + return request({ + url: 'api/dictDetail', + method: 'put', + data + }) +} + +export default { add, edit, del } diff --git a/eladmin-web/src/api/system/job.js b/eladmin-web/src/api/system/job.js new file mode 100644 index 0000000..a00630e --- /dev/null +++ b/eladmin-web/src/api/system/job.js @@ -0,0 +1,40 @@ +import request from '@/utils/request' + +export function getAllJob() { + const params = { + page: 0, + size: 9999, + enabled: true + } + return request({ + url: 'api/job', + method: 'get', + params + }) +} + +export function add(data) { + return request({ + url: 'api/job', + method: 'post', + data + }) +} + +export function del(ids) { + return request({ + url: 'api/job', + method: 'delete', + data: ids + }) +} + +export function edit(data) { + return request({ + url: 'api/job', + method: 'put', + data + }) +} + +export default { add, edit, del } diff --git a/eladmin-web/src/api/system/menu.js b/eladmin-web/src/api/system/menu.js new file mode 100644 index 0000000..282dd8d --- /dev/null +++ b/eladmin-web/src/api/system/menu.js @@ -0,0 +1,65 @@ +import request from '@/utils/request' + +export function getMenusTree(pid) { + return request({ + url: 'api/menus/lazy?pid=' + pid, + method: 'get' + }) +} + +export function getMenus(params) { + return request({ + url: 'api/menus', + method: 'get', + params + }) +} + +export function getMenuSuperior(ids) { + const data = Array.isArray(ids) || ids.length === 0 ? ids : Array.of(ids) + return request({ + url: 'api/menus/superior', + method: 'post', + data + }) +} + +export function getChild(id) { + return request({ + url: 'api/menus/child?id=' + id, + method: 'get' + }) +} + +export function buildMenus() { + return request({ + url: 'api/menus/build', + method: 'get' + }) +} + +export function add(data) { + return request({ + url: 'api/menus', + method: 'post', + data + }) +} + +export function del(ids) { + return request({ + url: 'api/menus', + method: 'delete', + data: ids + }) +} + +export function edit(data) { + return request({ + url: 'api/menus', + method: 'put', + data + }) +} + +export default { add, edit, del, getMenusTree, getMenuSuperior, getMenus, getChild } diff --git a/eladmin-web/src/api/system/purchase.js b/eladmin-web/src/api/system/purchase.js new file mode 100644 index 0000000..719bbc7 --- /dev/null +++ b/eladmin-web/src/api/system/purchase.js @@ -0,0 +1,27 @@ +import request from '@/utils/request' + +export function add(data) { + return request({ + url: 'api/purchase', + method: 'post', + data + }) +} + +export function del(ids) { + return request({ + url: 'api/purchase/', + method: 'delete', + data: ids + }) +} + +export function edit(data) { + return request({ + url: 'api/purchase', + method: 'put', + data + }) +} + +export default { add, edit, del } diff --git a/eladmin-web/src/api/system/reimburse.js b/eladmin-web/src/api/system/reimburse.js new file mode 100644 index 0000000..dfadcd9 --- /dev/null +++ b/eladmin-web/src/api/system/reimburse.js @@ -0,0 +1,27 @@ +import request from '@/utils/request' + +export function add(data) { + return request({ + url: 'api/reimburse', + method: 'post', + data + }) +} + +export function del(ids) { + return request({ + url: 'api/reimburse/', + method: 'delete', + data: ids + }) +} + +export function edit(data) { + return request({ + url: 'api/reimburse', + method: 'put', + data + }) +} + +export default { add, edit, del } diff --git a/eladmin-web/src/api/system/role.js b/eladmin-web/src/api/system/role.js new file mode 100644 index 0000000..1f7bc1e --- /dev/null +++ b/eladmin-web/src/api/system/role.js @@ -0,0 +1,57 @@ +import request from '@/utils/request' + +// 获取所有的Role +export function getAll() { + return request({ + url: 'api/roles/all', + method: 'get' + }) +} + +export function add(data) { + return request({ + url: 'api/roles', + method: 'post', + data + }) +} + +export function get(id) { + return request({ + url: 'api/roles/' + id, + method: 'get' + }) +} + +export function getLevel() { + return request({ + url: 'api/roles/level', + method: 'get' + }) +} + +export function del(ids) { + return request({ + url: 'api/roles', + method: 'delete', + data: ids + }) +} + +export function edit(data) { + return request({ + url: 'api/roles', + method: 'put', + data + }) +} + +export function editMenu(data) { + return request({ + url: 'api/roles/menu', + method: 'put', + data + }) +} + +export default { add, edit, del, get, editMenu, getLevel } diff --git a/eladmin-web/src/api/system/timing.js b/eladmin-web/src/api/system/timing.js new file mode 100644 index 0000000..613e15f --- /dev/null +++ b/eladmin-web/src/api/system/timing.js @@ -0,0 +1,41 @@ +import request from '@/utils/request' + +export function add(data) { + return request({ + url: 'api/jobs', + method: 'post', + data + }) +} + +export function del(ids) { + return request({ + url: 'api/jobs', + method: 'delete', + data: ids + }) +} + +export function edit(data) { + return request({ + url: 'api/jobs', + method: 'put', + data + }) +} + +export function updateIsPause(id) { + return request({ + url: 'api/jobs/' + id, + method: 'put' + }) +} + +export function execution(id) { + return request({ + url: 'api/jobs/exec/' + id, + method: 'put' + }) +} + +export default { del, updateIsPause, execution, add, edit } diff --git a/eladmin-web/src/api/system/user.js b/eladmin-web/src/api/system/user.js new file mode 100644 index 0000000..c14df11 --- /dev/null +++ b/eladmin-web/src/api/system/user.js @@ -0,0 +1,69 @@ +import request from '@/utils/request' +import { encrypt } from '@/utils/rsaEncrypt' + +export function add(data) { + return request({ + url: 'api/users', + method: 'post', + data + }) +} + +export function del(ids) { + return request({ + url: 'api/users', + method: 'delete', + data: ids + }) +} + +export function edit(data) { + return request({ + url: 'api/users', + method: 'put', + data + }) +} + +export function editUser(data) { + return request({ + url: 'api/users/center', + method: 'put', + data + }) +} + +export function updatePass(user) { + const data = { + oldPass: encrypt(user.oldPass), + newPass: encrypt(user.newPass) + } + return request({ + url: 'api/users/updatePass/', + method: 'post', + data + }) +} + +export function resetPwd(ids) { + return request({ + url: 'api/users/resetPwd', + method: 'put', + data: ids + }) +} + +export function updateEmail(form) { + const data = { + password: encrypt(form.pass), + email: form.email + } + return request({ + url: 'api/users/updateEmail/' + form.code, + method: 'post', + data + }) +} + +export default { add, edit, del, resetPwd } + diff --git a/eladmin-web/src/api/tools/alipay.js b/eladmin-web/src/api/tools/alipay.js new file mode 100644 index 0000000..54090f5 --- /dev/null +++ b/eladmin-web/src/api/tools/alipay.js @@ -0,0 +1,25 @@ +import request from '@/utils/request' + +export function get() { + return request({ + url: 'api/aliPay', + method: 'get' + }) +} + +export function update(data) { + return request({ + url: 'api/aliPay', + data, + method: 'put' + }) +} + +// 支付 +export function toAliPay(url, data) { + return request({ + url: 'api/' + url, + data, + method: 'post' + }) +} diff --git a/eladmin-web/src/api/tools/email.js b/eladmin-web/src/api/tools/email.js new file mode 100644 index 0000000..af030cb --- /dev/null +++ b/eladmin-web/src/api/tools/email.js @@ -0,0 +1,24 @@ +import request from '@/utils/request' + +export function get() { + return request({ + url: 'api/email', + method: 'get' + }) +} + +export function update(data) { + return request({ + url: 'api/email', + data, + method: 'put' + }) +} + +export function send(data) { + return request({ + url: 'api/email', + data, + method: 'post' + }) +} diff --git a/eladmin-web/src/api/tools/localStorage.js b/eladmin-web/src/api/tools/localStorage.js new file mode 100644 index 0000000..63ebe2b --- /dev/null +++ b/eladmin-web/src/api/tools/localStorage.js @@ -0,0 +1,27 @@ +import request from '@/utils/request' + +export function add(data) { + return request({ + url: 'api/localStorage', + method: 'post', + data + }) +} + +export function del(ids) { + return request({ + url: 'api/localStorage/', + method: 'delete', + data: ids + }) +} + +export function edit(data) { + return request({ + url: 'api/localStorage', + method: 'put', + data + }) +} + +export default { add, edit, del } diff --git a/eladmin-web/src/api/tools/qiniu.js b/eladmin-web/src/api/tools/qiniu.js new file mode 100644 index 0000000..6d56771 --- /dev/null +++ b/eladmin-web/src/api/tools/qiniu.js @@ -0,0 +1,40 @@ +import request from '@/utils/request' + +export function get() { + return request({ + url: 'api/qiNiuContent/config', + method: 'get' + }) +} + +export function update(data) { + return request({ + url: 'api/qiNiuContent/config', + data, + method: 'put' + }) +} + +export function download(id) { + return request({ + url: 'api/qiNiuContent/download/' + id, + method: 'get' + }) +} + +export function sync() { + return request({ + url: 'api/qiNiuContent/synchronize', + method: 'post' + }) +} + +export function del(ids) { + return request({ + url: 'api/qiNiuContent', + method: 'delete', + data: ids + }) +} + +export default { del, download, sync } diff --git a/eladmin-web/src/api/userBank.js b/eladmin-web/src/api/userBank.js new file mode 100644 index 0000000..b88cdd8 --- /dev/null +++ b/eladmin-web/src/api/userBank.js @@ -0,0 +1,27 @@ +import request from '@/utils/request' + +export function add(data) { + return request({ + url: 'api/userBank', + method: 'post', + data + }) +} + +export function del(ids) { + return request({ + url: 'api/userBank/', + method: 'delete', + data: ids + }) +} + +export function edit(data) { + return request({ + url: 'api/userBank', + method: 'put', + data + }) +} + +export default { add, edit, del } diff --git a/eladmin-web/src/assets/401_images/401.gif b/eladmin-web/src/assets/401_images/401.gif new file mode 100644 index 0000000000000000000000000000000000000000..cd6e0d9433421b3f29d0ec0c40f755e354728000 GIT binary patch literal 164227 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⋘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 GY5fOuZZib{ literal 0 HcmV?d00001 diff --git a/eladmin-web/src/assets/404_images/404.png b/eladmin-web/src/assets/404_images/404.png new file mode 100644 index 0000000000000000000000000000000000000000..3d8e2305cc973ad2121403aee4bf08728f76c461 GIT binary patch literal 98071 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 literal 0 HcmV?d00001 diff --git a/eladmin-web/src/assets/404_images/404_cloud.png b/eladmin-web/src/assets/404_images/404_cloud.png new file mode 100644 index 0000000000000000000000000000000000000000..c6281d09013e0a2c5f8e699a0a6038d9480291e5 GIT binary patch literal 4766 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 literal 0 HcmV?d00001 diff --git a/eladmin-web/src/assets/icons/index.js b/eladmin-web/src/assets/icons/index.js new file mode 100644 index 0000000..2c6b309 --- /dev/null +++ b/eladmin-web/src/assets/icons/index.js @@ -0,0 +1,9 @@ +import Vue from 'vue' +import SvgIcon from '@/components/SvgIcon'// svg component + +// register globally +Vue.component('svg-icon', SvgIcon) + +const req = require.context('./svg', false, /\.svg$/) +const requireAll = requireContext => requireContext.keys().map(requireContext) +requireAll(req) diff --git a/eladmin-web/src/assets/icons/svg/Steve-Jobs.svg b/eladmin-web/src/assets/icons/svg/Steve-Jobs.svg new file mode 100644 index 0000000..53843e2 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/Steve-Jobs.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/alipay.svg b/eladmin-web/src/assets/icons/svg/alipay.svg new file mode 100644 index 0000000..9138981 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/alipay.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/anq.svg b/eladmin-web/src/assets/icons/svg/anq.svg new file mode 100644 index 0000000..a466608 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/anq.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/app.svg b/eladmin-web/src/assets/icons/svg/app.svg new file mode 100644 index 0000000..0796da3 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/app.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/backup.svg b/eladmin-web/src/assets/icons/svg/backup.svg new file mode 100644 index 0000000..a3272a4 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/backup.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/blog.svg b/eladmin-web/src/assets/icons/svg/blog.svg new file mode 100644 index 0000000..a990eba --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/blog.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/chain.svg b/eladmin-web/src/assets/icons/svg/chain.svg new file mode 100644 index 0000000..ed3317f --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/chain.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/chart.svg b/eladmin-web/src/assets/icons/svg/chart.svg new file mode 100644 index 0000000..27728fb --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/chart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/codeConsole.svg b/eladmin-web/src/assets/icons/svg/codeConsole.svg new file mode 100644 index 0000000..672ec6e --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/codeConsole.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/dashboard.svg b/eladmin-web/src/assets/icons/svg/dashboard.svg new file mode 100644 index 0000000..5317d37 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/dashboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/database.svg b/eladmin-web/src/assets/icons/svg/database.svg new file mode 100644 index 0000000..7fbad9b --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/database.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/date.svg b/eladmin-web/src/assets/icons/svg/date.svg new file mode 100644 index 0000000..0540e99 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/date.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/deploy.svg b/eladmin-web/src/assets/icons/svg/deploy.svg new file mode 100644 index 0000000..f4a1c56 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/deploy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/dept.svg b/eladmin-web/src/assets/icons/svg/dept.svg new file mode 100644 index 0000000..894e4bf --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/dept.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/dev.svg b/eladmin-web/src/assets/icons/svg/dev.svg new file mode 100644 index 0000000..ed4d23c --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/dev.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/develop.svg b/eladmin-web/src/assets/icons/svg/develop.svg new file mode 100644 index 0000000..e189223 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/develop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/dictionary.svg b/eladmin-web/src/assets/icons/svg/dictionary.svg new file mode 100644 index 0000000..6e83c43 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/dictionary.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/doc.svg b/eladmin-web/src/assets/icons/svg/doc.svg new file mode 100644 index 0000000..9160de8 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/doc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/download.svg b/eladmin-web/src/assets/icons/svg/download.svg new file mode 100644 index 0000000..0243c6a --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/edit.svg b/eladmin-web/src/assets/icons/svg/edit.svg new file mode 100644 index 0000000..d26101f --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/education.svg b/eladmin-web/src/assets/icons/svg/education.svg new file mode 100644 index 0000000..7bfb01d --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/education.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/email.svg b/eladmin-web/src/assets/icons/svg/email.svg new file mode 100644 index 0000000..f1cf3ae --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/email.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/error.svg b/eladmin-web/src/assets/icons/svg/error.svg new file mode 100644 index 0000000..fd935da --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/exit-fullscreen.svg b/eladmin-web/src/assets/icons/svg/exit-fullscreen.svg new file mode 100644 index 0000000..485c128 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/exit-fullscreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/fullscreen.svg b/eladmin-web/src/assets/icons/svg/fullscreen.svg new file mode 100644 index 0000000..0e86b6f --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/fullscreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/fwb.svg b/eladmin-web/src/assets/icons/svg/fwb.svg new file mode 100644 index 0000000..59933fc --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/fwb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/github.svg b/eladmin-web/src/assets/icons/svg/github.svg new file mode 100644 index 0000000..8145e95 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/gonggao.svg b/eladmin-web/src/assets/icons/svg/gonggao.svg new file mode 100644 index 0000000..22aed08 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/gonggao.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/icon.svg b/eladmin-web/src/assets/icons/svg/icon.svg new file mode 100644 index 0000000..82fbdd9 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/image.svg b/eladmin-web/src/assets/icons/svg/image.svg new file mode 100644 index 0000000..16d572f --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/index.svg b/eladmin-web/src/assets/icons/svg/index.svg new file mode 100644 index 0000000..fdb3826 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/index.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/international.svg b/eladmin-web/src/assets/icons/svg/international.svg new file mode 100644 index 0000000..e9b56ee --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/international.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/ipvisits.svg b/eladmin-web/src/assets/icons/svg/ipvisits.svg new file mode 100644 index 0000000..4ca473d --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/ipvisits.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/java.svg b/eladmin-web/src/assets/icons/svg/java.svg new file mode 100644 index 0000000..e2effbb --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/java.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/link.svg b/eladmin-web/src/assets/icons/svg/link.svg new file mode 100644 index 0000000..48197ba --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/list.svg b/eladmin-web/src/assets/icons/svg/list.svg new file mode 100644 index 0000000..20259ed --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/list.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/lock.svg b/eladmin-web/src/assets/icons/svg/lock.svg new file mode 100644 index 0000000..0f842ea --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/log.svg b/eladmin-web/src/assets/icons/svg/log.svg new file mode 100644 index 0000000..4fefe74 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/log.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/login.svg b/eladmin-web/src/assets/icons/svg/login.svg new file mode 100644 index 0000000..cc5a854 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/login.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/markdown.svg b/eladmin-web/src/assets/icons/svg/markdown.svg new file mode 100644 index 0000000..7cd6747 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/markdown.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/menu.svg b/eladmin-web/src/assets/icons/svg/menu.svg new file mode 100644 index 0000000..e4360a0 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/menu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/message.svg b/eladmin-web/src/assets/icons/svg/message.svg new file mode 100644 index 0000000..14ca817 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/message.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/mnt.svg b/eladmin-web/src/assets/icons/svg/mnt.svg new file mode 100644 index 0000000..936ce29 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/mnt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/money.svg b/eladmin-web/src/assets/icons/svg/money.svg new file mode 100644 index 0000000..c1580de --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/money.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/monitor.svg b/eladmin-web/src/assets/icons/svg/monitor.svg new file mode 100644 index 0000000..339370a --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/monitor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/nested.svg b/eladmin-web/src/assets/icons/svg/nested.svg new file mode 100644 index 0000000..06713a8 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/nested.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/password.svg b/eladmin-web/src/assets/icons/svg/password.svg new file mode 100644 index 0000000..4ab451f --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/password.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/people.svg b/eladmin-web/src/assets/icons/svg/people.svg new file mode 100644 index 0000000..2bd54ae --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/people.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/peoples.svg b/eladmin-web/src/assets/icons/svg/peoples.svg new file mode 100644 index 0000000..2c91161 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/peoples.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/permission.svg b/eladmin-web/src/assets/icons/svg/permission.svg new file mode 100644 index 0000000..c4c7409 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/permission.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/phone.svg b/eladmin-web/src/assets/icons/svg/phone.svg new file mode 100644 index 0000000..da339f9 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/phone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/qiniu.svg b/eladmin-web/src/assets/icons/svg/qiniu.svg new file mode 100644 index 0000000..c2f9f8b --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/qiniu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/redis.svg b/eladmin-web/src/assets/icons/svg/redis.svg new file mode 100644 index 0000000..bef111b --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/redis.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/role.svg b/eladmin-web/src/assets/icons/svg/role.svg new file mode 100644 index 0000000..76cb18f --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/role.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/search.svg b/eladmin-web/src/assets/icons/svg/search.svg new file mode 100644 index 0000000..84233dd --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/server.svg b/eladmin-web/src/assets/icons/svg/server.svg new file mode 100644 index 0000000..db6dcdf --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/server.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/shopping.svg b/eladmin-web/src/assets/icons/svg/shopping.svg new file mode 100644 index 0000000..87513e7 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/shopping.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/size.svg b/eladmin-web/src/assets/icons/svg/size.svg new file mode 100644 index 0000000..ddb25b8 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/size.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/skill.svg b/eladmin-web/src/assets/icons/svg/skill.svg new file mode 100644 index 0000000..a3b7312 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/skill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/source.svg b/eladmin-web/src/assets/icons/svg/source.svg new file mode 100644 index 0000000..1c3a038 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/source.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/sqlMonitor.svg b/eladmin-web/src/assets/icons/svg/sqlMonitor.svg new file mode 100644 index 0000000..950a430 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/sqlMonitor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/swagger.svg b/eladmin-web/src/assets/icons/svg/swagger.svg new file mode 100644 index 0000000..ded7de8 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/swagger.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/sys-tools.svg b/eladmin-web/src/assets/icons/svg/sys-tools.svg new file mode 100644 index 0000000..8f9055e --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/sys-tools.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/system.svg b/eladmin-web/src/assets/icons/svg/system.svg new file mode 100644 index 0000000..9333c60 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/system.svg @@ -0,0 +1 @@ + diff --git a/eladmin-web/src/assets/icons/svg/system1.svg b/eladmin-web/src/assets/icons/svg/system1.svg new file mode 100644 index 0000000..37b0a0a --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/system1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/tab.svg b/eladmin-web/src/assets/icons/svg/tab.svg new file mode 100644 index 0000000..b4b48e4 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/tab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/theme.svg b/eladmin-web/src/assets/icons/svg/theme.svg new file mode 100644 index 0000000..5982a2f --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/theme.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/timing.svg b/eladmin-web/src/assets/icons/svg/timing.svg new file mode 100644 index 0000000..f8fdc6d --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/timing.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/tools.svg b/eladmin-web/src/assets/icons/svg/tools.svg new file mode 100644 index 0000000..aba1a40 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/tools.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/tree-table.svg b/eladmin-web/src/assets/icons/svg/tree-table.svg new file mode 100644 index 0000000..8aafdb8 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/tree-table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/tree.svg b/eladmin-web/src/assets/icons/svg/tree.svg new file mode 100644 index 0000000..dd4b7dd --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/tree.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/unlock.svg b/eladmin-web/src/assets/icons/svg/unlock.svg new file mode 100644 index 0000000..1219e41 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/unlock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/user.svg b/eladmin-web/src/assets/icons/svg/user.svg new file mode 100644 index 0000000..09d7a81 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/user1.svg b/eladmin-web/src/assets/icons/svg/user1.svg new file mode 100644 index 0000000..14ca51e --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/user1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/validCode.svg b/eladmin-web/src/assets/icons/svg/validCode.svg new file mode 100644 index 0000000..a1feb74 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/validCode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/visits.svg b/eladmin-web/src/assets/icons/svg/visits.svg new file mode 100644 index 0000000..8425662 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/visits.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/web.svg b/eladmin-web/src/assets/icons/svg/web.svg new file mode 100644 index 0000000..9c57415 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/web.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/wechat.svg b/eladmin-web/src/assets/icons/svg/wechat.svg new file mode 100644 index 0000000..c586e55 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/wechat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/weixin.svg b/eladmin-web/src/assets/icons/svg/weixin.svg new file mode 100644 index 0000000..8dbcfa5 --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/weixin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svg/zujian.svg b/eladmin-web/src/assets/icons/svg/zujian.svg new file mode 100644 index 0000000..2aba32f --- /dev/null +++ b/eladmin-web/src/assets/icons/svg/zujian.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eladmin-web/src/assets/icons/svgo.yml b/eladmin-web/src/assets/icons/svgo.yml new file mode 100644 index 0000000..d11906a --- /dev/null +++ b/eladmin-web/src/assets/icons/svgo.yml @@ -0,0 +1,22 @@ +# replace default config + +# multipass: true +# full: true + +plugins: + + # - name + # + # or: + # - name: false + # - name: true + # + # or: + # - name: + # param1: 1 + # param2: 2 + +- removeAttrs: + attrs: + - 'fill' + - 'fill-rule' diff --git a/eladmin-web/src/assets/images/avatar.png b/eladmin-web/src/assets/images/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..997732a452cdf6273ec7cd444dab69f1a4db4e48 GIT binary patch literal 1865 zcmZ`)c{tQtAODTza*@X1N<*aVWTtDs?k%!jYoY}WLiX#HElY(Ex>s3Bwz^c7C|g8? z;b+D&Gxl}JF!ptZGRzore|q14-ap>wIp_QRe3#GXoaa2}IVai9#_Wi|aRC4Tj#!u* z+e7}&;pFFrtneos1#&#zhSr7vQ1w`F-<=nVWj)O8tpOlX0RUp+0e}gmVwM0P6a@e) zZUCVF8~`N#%=>iF003aFc2*81P{Z`}G!{o_>mXq}h^-w&$bKIL2)A|+4l$mH#r-!= zAP}%P5*8%1f&?;|j0K@Q)Y%JxR*=|^?}cnDp45&%q~mbI{}JSHIN0t%NUE)aaEL*u z)OV;162Q(w1!#3h5DLF1ziWa7QvcZO+|nj(>)Q@ve}5k;O-)nx_Lz{u(C8#%caJi= zu)MO)V(rh)EwO(}N?EEEcPqIJCY&TU)1% zO-#4rNxQpy)cNHe(lDESu(-UovbxdH)i*Ug$6)Nzx4v#|f9oF{pPrfD+Wy)}8067L z2}4KxbBMjoC4gVuA*!&xV_X2x>YyQew!>{XkiHnIhi8sMb%8Z5G#cQ99jChoe)cI+=tDU$U#pbw*z+#CC8P=$~EobKL z=6ZU6$+Z$o5Lef3Y+t_pF>ZCMx;fa2` zW7kY|HT}}U2o-W?N)FJfO;ph&q$qW*DMJMTgr=v3sUS>^;Hg$sG#j%tlyR}-ZLAO? zN%GH~=ZvI z%3ipaV_Ex(faW=BW~>Y$cTlJk##zBHsnU!qe2@Kd7ND;W@gMtlm8a0)YTv~* zuUQ~+tkg92wzP86iLl@TH${)%UPs#?DefE99ukizBJlGmODZDaHT>EKPp-X%J<~ME zu%EL<$WOkF(hx<5J2gG{Xx`MRkgAt(Ub0!P>fyy_+ZHd+8l(&iVEy>-fSB6mslu_b zcjl>#yN@cXc>L}ex+338UtY^H5xm6xaQ1@_TkUzBlkQX9^tgJ&(+g6LPV?}OXYof3 zX-WZJFYvjxTf2(3FRR33Gqn?4gMPhV;7fKMGb67yNGBFd97_skgaju9mn9EG{UQWX zN=Xv@^eQQn8jYDtMSP)y~~w$$XsMrgg@)x+Kd)0m`id< zCn(vd&gN^;W7$)m#3d+K@QR@3*b@cYFR;WA!*sCdkIt6bp1h#w@|K6Ee~ff87(k~` z?`8W-)VJ-!p8Qm5M4_D;ub{XPnTns_|5&_a$C)~W-&xNpFcR(FfN z9jeTdW$_E7nXqiMpbO4Br^zw;wM^dqlVtl~a?Y>CKSmZNG{`nwP|vDy;@+~iB45cj zFV8sPdcxic*;Rz`mD)9}D&_HL4sN>;y_bK3z`jgC@tjk7!@oV-qa|}I);=1ot~0M? zBbC-;T{IKj}RiTaFBbVESTn0$!I z)eukj5HEd?+g^|Z&Z5wI>Sz=+98hQVwe<8gbyZO)eH1Fd=cU8{5CVfdeQ!qme}c4@ SBJ}40fQ5;T@p~h;xPJj%?#LPd literal 0 HcmV?d00001 diff --git a/eladmin-web/src/assets/images/background.webp b/eladmin-web/src/assets/images/background.webp new file mode 100644 index 0000000000000000000000000000000000000000..3ad73c8b209ac4563e2ad6bb7cab860c87e7fa32 GIT binary patch literal 156066 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

VF=Zgy&r>d@BHMi^U7vJ0a=P`8NnM@)}IX7Hy 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 D!1tpc literal 0 HcmV?d00001 diff --git a/eladmin-web/src/assets/images/logo.png b/eladmin-web/src/assets/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f7577109651b89def3d9359ab324b445c714fe77 GIT binary patch literal 8852 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 ZLF@BRes2KN|GQZNP*u`YtbtmE{|_Y7!}b6G literal 0 HcmV?d00001 diff --git a/eladmin-web/src/assets/styles/btn.scss b/eladmin-web/src/assets/styles/btn.scss new file mode 100644 index 0000000..8f47f2c --- /dev/null +++ b/eladmin-web/src/assets/styles/btn.scss @@ -0,0 +1,99 @@ +@import 'variables'; + +@mixin colorBtn($color) { + background: $color; + + &:hover { + color: $color; + + &:before, + &:after { + background: $color; + } + } +} + +.blue-btn { + @include colorBtn($blue) +} + +.light-blue-btn { + @include colorBtn($light-blue) +} + +.red-btn { + @include colorBtn($red) +} + +.pink-btn { + @include colorBtn($pink) +} + +.green-btn { + @include colorBtn($green) +} + +.tiffany-btn { + @include colorBtn($tiffany) +} + +.yellow-btn { + @include colorBtn($yellow) +} + +.pan-btn { + font-size: 14px; + color: #fff; + padding: 14px 36px; + border-radius: 8px; + border: none; + outline: none; + transition: 600ms ease all; + position: relative; + display: inline-block; + + &:hover { + background: #fff; + + &:before, + &:after { + width: 100%; + transition: 600ms ease all; + } + } + + &:before, + &:after { + content: ''; + position: absolute; + top: 0; + right: 0; + height: 2px; + width: 0; + transition: 400ms ease all; + } + + &::after { + right: inherit; + top: inherit; + left: 0; + bottom: 0; + } +} + +.custom-button { + display: inline-block; + line-height: 1; + white-space: nowrap; + cursor: pointer; + background: #fff; + color: #fff; + -webkit-appearance: none; + text-align: center; + box-sizing: border-box; + outline: 0; + margin: 0; + padding: 10px 15px; + font-size: 14px; + border-radius: 4px; +} diff --git a/eladmin-web/src/assets/styles/eladmin.scss b/eladmin-web/src/assets/styles/eladmin.scss new file mode 100644 index 0000000..e1e0195 --- /dev/null +++ b/eladmin-web/src/assets/styles/eladmin.scss @@ -0,0 +1,117 @@ +.head-container { + padding-bottom: 10px; + .filter-item { + display: inline-block; + vertical-align: middle; + margin: 0 3px 10px 0; + input { + height: 30.5px; + line-height: 30.5px; + } + } + .el-form-item-label { + margin: 0 3px 9px 0; + display: inline-block; + text-align: right; + vertical-align: middle; + font-size: 14px; + color: #606266; + line-height: 30.5px; + padding: 0 7px 0 7px; + } + .el-button+.el-button { + margin-left: 0 !important; + } + .el-select__caret.el-input__icon.el-icon-arrow-up{ + line-height: 30.5px; + } + .date-item { + display: inline-block; + vertical-align: middle; + margin-bottom: 10px; + height: 30.5px !important; + width: 230px !important; + } +} +.el-avatar { + display: inline-block; + text-align: center; + background: #ccc; + color: #fff; + white-space: nowrap; + position: relative; + overflow: hidden; + vertical-align: middle; + width: 32px; + height: 32px; + line-height: 32px; + border-radius: 16px; +} + +.logo-con{ + height: 60px; + padding: 13px 0 0; + img{ + height: 32px; + width: 135px; + display: block; + //margin: 0 auto; + } +} + +#el-login-footer { + height: 40px; + line-height: 40px; + position: fixed; + bottom: 0; + width: 100%; + text-align: center; + color: #fff; + font-family: Arial, serif; + font-size: 12px; + letter-spacing: 1px; +} + +#el-main-footer { + background: none repeat scroll 0 0 white; + border-top: 1px solid #e7eaec; + overflow: hidden; + padding: 10px 6px 0 6px; + height: 33px; + font-size: 0.7rem !important; + color: #7a8b9a; + letter-spacing: 0.8px; + font-family: Arial, sans-serif !important; + position: fixed; + bottom: 0; + z-index: 99; + width: 100%; +} +.eladmin-upload { + border: 1px dashed #c0ccda; + border-radius: 5px; + height: 45px; + line-height: 45px; + width: 368px; +} +.my-blockquote{ + margin: 0 0 10px; + padding: 15px; + line-height: 22px; + border-left: 5px solid #00437B; + border-radius: 0 2px 2px 0; + background-color: #f2f2f2; +} +.my-code{ + position: relative; + padding: 15px; + line-height: 20px; + border-left: 5px solid #ddd; + color: #333; + font-family: Courier New, serif; + font-size: 12px +} + +.el-tabs{ + margin-bottom: 25px; +} diff --git a/eladmin-web/src/assets/styles/element-ui.scss b/eladmin-web/src/assets/styles/element-ui.scss new file mode 100644 index 0000000..8f7881c --- /dev/null +++ b/eladmin-web/src/assets/styles/element-ui.scss @@ -0,0 +1,79 @@ +// cover some element-ui styles + +.el-breadcrumb__inner, +.el-breadcrumb__inner a { + font-weight: 400 !important; +} + +.el-upload { + input[type="file"] { + display: none !important; + } +} + +.el-upload__input { + display: none; +} + +.cell { + .el-tag { + margin-right: 0; + } +} + +.small-padding { + .cell { + padding-left: 5px; + padding-right: 5px; + } +} + +.fixed-width { + .el-button--mini { + padding: 7px 10px; + width: 60px; + } +} + +.status-col { + .cell { + padding: 0 10px; + text-align: center; + + .el-tag { + margin-right: 0; + } + } +} + +// to fixed https://github.com/ElemeFE/element/issues/2461 +.el-dialog { + transform: none; + left: 0; + position: relative; + margin: 0 auto; +} + +// refine element ui upload +.upload-container { + .el-upload { + width: 100%; + + .el-upload-dragger { + width: 100%; + height: 200px; + } + } +} + +// dropdown +.el-dropdown-menu { + a { + display: block + } +} + +// fix date-picker ui bug in filter-item +.el-range-editor.el-input__inner { + display: inline-flex !important; +} diff --git a/eladmin-web/src/assets/styles/element-variables.scss b/eladmin-web/src/assets/styles/element-variables.scss new file mode 100644 index 0000000..a4f8c4a --- /dev/null +++ b/eladmin-web/src/assets/styles/element-variables.scss @@ -0,0 +1,31 @@ +/** +* I think element-ui's default theme color is too light for long-term use. +* So I modified the default color and you can modify it to your liking. +**/ + +/* theme color */ +$--color-primary: #1890ff; +$--color-success: #13ce66; +$--color-warning: #FFBA00; +$--color-danger: #ff4949; +// $--color-info: #1E1E1E; + +$--button-font-weight: 400; + +// $--color-text-regular: #1f2d3d; + +$--border-color-light: #dfe4ed; +$--border-color-lighter: #e6ebf5; + +$--table-border:1px solid#dfe6ec; + +/* icon font path, required */ +$--font-path: '~element-ui/lib/theme-chalk/fonts'; + +@import "../../../node_modules/element-ui/packages/theme-chalk/src/index"; + +// the :export directive is the magic sauce for webpack +// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass +:export { + theme: $--color-primary; +} diff --git a/eladmin-web/src/assets/styles/index.scss b/eladmin-web/src/assets/styles/index.scss new file mode 100644 index 0000000..21fbda1 --- /dev/null +++ b/eladmin-web/src/assets/styles/index.scss @@ -0,0 +1,182 @@ +@import 'variables'; +@import 'mixin'; +@import 'transition'; +@import 'element-ui'; +@import 'sidebar'; +@import 'btn'; +@import 'eladmin'; + +body { + height: 100%; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; +} + +label { + font-weight: 700; +} + +html { + height: 100%; + box-sizing: border-box; +} + +#app { + height: 100%; +} + +*, +*:before, +*:after { + box-sizing: inherit; +} + +.no-padding { + padding: 0 !important; +} + +.padding-content { + padding: 4px 0; +} + +a:focus, +a:active { + outline: none; +} + +a, +a:focus, +a:hover { + cursor: pointer; + color: inherit; + text-decoration: none; +} + +div:focus { + outline: none; +} + +.fr { + float: right; +} + +.fl { + float: left; +} + +.pr-5 { + padding-right: 5px; +} + +.pl-5 { + padding-left: 5px; +} + +.block { + display: block; +} + +.pointer { + cursor: pointer; +} + +.inlineBlock { + display: block; +} + +.clearfix { + &:after { + visibility: hidden; + display: block; + font-size: 0; + content: " "; + clear: both; + height: 0; + } +} + +aside { + background: #eef1f6; + padding: 8px 24px; + margin-bottom: 20px; + border-radius: 2px; + display: block; + line-height: 32px; + font-size: 16px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + color: #2c3e50; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + a { + color: #337ab7; + cursor: pointer; + + &:hover { + color: rgb(32, 160, 255); + } + } +} + +//main-container全局样式 +.app-container { + padding: 20px 20px 45px 20px; +} + +.components-container { + margin: 30px 50px; + position: relative; +} + +.pagination-container { + margin-top: 30px; +} + +.text-center { + text-align: center +} + +.sub-navbar { + height: 50px; + line-height: 50px; + position: relative; + width: 100%; + text-align: right; + padding-right: 20px; + transition: 600ms ease position; + background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%); + + .subtitle { + font-size: 20px; + color: #fff; + } + + &.draft { + background: #d0d0d0; + } + + &.deleted { + background: #d0d0d0; + } +} + +.link-type, +.link-type:focus { + color: #337ab7; + cursor: pointer; + + &:hover { + color: rgb(32, 160, 255); + } +} + +//refine vue-multiselect plugin +.multiselect { + line-height: 16px; +} + +.multiselect--active { + z-index: 1000 !important; +} diff --git a/eladmin-web/src/assets/styles/mixin.scss b/eladmin-web/src/assets/styles/mixin.scss new file mode 100644 index 0000000..06fa061 --- /dev/null +++ b/eladmin-web/src/assets/styles/mixin.scss @@ -0,0 +1,66 @@ +@mixin clearfix { + &:after { + content: ""; + display: table; + clear: both; + } +} + +@mixin scrollBar { + &::-webkit-scrollbar-track-piece { + background: #d3dce6; + } + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + background: #99a9bf; + border-radius: 20px; + } +} + +@mixin relative { + position: relative; + width: 100%; + height: 100%; +} + +@mixin pct($pct) { + width: #{$pct}; + position: relative; + margin: 0 auto; +} + +@mixin triangle($width, $height, $color, $direction) { + $width: $width/2; + $color-border-style: $height solid $color; + $transparent-border-style: $width solid transparent; + height: 0; + width: 0; + + @if $direction==up { + border-bottom: $color-border-style; + border-left: $transparent-border-style; + border-right: $transparent-border-style; + } + + @else if $direction==right { + border-left: $color-border-style; + border-top: $transparent-border-style; + border-bottom: $transparent-border-style; + } + + @else if $direction==down { + border-top: $color-border-style; + border-left: $transparent-border-style; + border-right: $transparent-border-style; + } + + @else if $direction==left { + border-right: $color-border-style; + border-top: $transparent-border-style; + border-bottom: $transparent-border-style; + } +} diff --git a/eladmin-web/src/assets/styles/sidebar.scss b/eladmin-web/src/assets/styles/sidebar.scss new file mode 100644 index 0000000..17381fc --- /dev/null +++ b/eladmin-web/src/assets/styles/sidebar.scss @@ -0,0 +1,209 @@ +#app { + + .main-container { + min-height: 100%; + transition: margin-left .28s; + margin-left: $sideBarWidth; + position: relative; + } + + .sidebar-container { + transition: width 0.28s; + width: $sideBarWidth !important; + background-color: $menuBg; + height: 100%; + position: fixed; + font-size: 0; + top: 0; + bottom: 0; + left: 0; + z-index: 1001; + overflow: hidden; + + // reset element-ui css + .horizontal-collapse-transition { + transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; + } + + .scrollbar-wrapper { + overflow-x: hidden !important; + } + + .el-scrollbar__bar.is-vertical { + right: 0; + } + + .el-scrollbar { + height: 100%; + } + + &.has-logo { + .el-scrollbar { + height: calc(100% - 50px); + } + } + + .is-horizontal { + display: none; + } + + a { + display: inline-block; + width: 100%; + overflow: hidden; + } + + .svg-icon { + margin-right: 16px; + } + + .el-menu { + border: none; + height: 100%; + width: 100% !important; + } + + // menu hover + .submenu-title-noDropdown, + .el-submenu__title { + &:hover { + background-color: $menuHover !important; + } + } + + .is-active>.el-submenu__title { + color: $subMenuActiveText !important; + } + + & .nest-menu .el-submenu>.el-submenu__title, + & .el-submenu .el-menu-item { + min-width: $sideBarWidth !important; + background-color: $subMenuBg !important; + + &:hover { + background-color: $subMenuHover !important; + } + } + } + + .hideSidebar { + .sidebar-container { + width: 54px !important; + } + + .main-container { + margin-left: 54px; + } + + .submenu-title-noDropdown { + padding: 0 !important; + position: relative; + + .el-tooltip { + padding: 0 !important; + + .svg-icon { + margin-left: 20px; + } + } + } + + .el-submenu { + overflow: hidden; + + &>.el-submenu__title { + padding: 0 !important; + + .svg-icon { + margin-left: 20px; + } + + .el-submenu__icon-arrow { + display: none; + } + } + } + + .el-menu--collapse { + .el-submenu { + &>.el-submenu__title { + &>span { + height: 0; + width: 0; + overflow: hidden; + visibility: hidden; + display: inline-block; + } + } + } + } + } + + .el-menu--collapse .el-menu .el-submenu { + min-width: $sideBarWidth !important; + } + + // mobile responsive + .mobile { + .main-container { + margin-left: 0; + } + + .sidebar-container { + transition: transform .28s; + width: $sideBarWidth !important; + } + + &.hideSidebar { + .sidebar-container { + pointer-events: none; + transition-duration: 0.3s; + transform: translate3d(-$sideBarWidth, 0, 0); + } + } + } + + .withoutAnimation { + + .main-container, + .sidebar-container { + transition: none; + } + } +} + +// when menu collapsed +.el-menu--vertical { + &>.el-menu { + .svg-icon { + margin-right: 16px; + } + } + + .nest-menu .el-submenu>.el-submenu__title, + .el-menu-item { + &:hover { + // you can use $subMenuHover + background-color: $menuHover !important; + } + } + + // the scroll bar appears when the subMenu is too long + >.el-menu--popup { + max-height: 100vh; + overflow-y: auto; + + &::-webkit-scrollbar-track-piece { + background: #d3dce6; + } + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + background: #99a9bf; + border-radius: 20px; + } + } +} diff --git a/eladmin-web/src/assets/styles/transition.scss b/eladmin-web/src/assets/styles/transition.scss new file mode 100644 index 0000000..4cb27cc --- /dev/null +++ b/eladmin-web/src/assets/styles/transition.scss @@ -0,0 +1,48 @@ +// global transition css + +/* fade */ +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.28s; +} + +.fade-enter, +.fade-leave-active { + opacity: 0; +} + +/* fade-transform */ +.fade-transform-leave-active, +.fade-transform-enter-active { + transition: all .5s; +} + +.fade-transform-enter { + opacity: 0; + transform: translateX(-30px); +} + +.fade-transform-leave-to { + opacity: 0; + transform: translateX(30px); +} + +/* breadcrumb transition */ +.breadcrumb-enter-active, +.breadcrumb-leave-active { + transition: all .5s; +} + +.breadcrumb-enter, +.breadcrumb-leave-active { + opacity: 0; + transform: translateX(20px); +} + +.breadcrumb-move { + transition: all .5s; +} + +.breadcrumb-leave-active { + position: absolute; +} diff --git a/eladmin-web/src/assets/styles/variables.scss b/eladmin-web/src/assets/styles/variables.scss new file mode 100644 index 0000000..41de79c --- /dev/null +++ b/eladmin-web/src/assets/styles/variables.scss @@ -0,0 +1,35 @@ +// base color +$blue:#324157; +$light-blue:#3A71A8; +$red:#C03639; +$pink: #E65D6E; +$green: #30B08F; +$tiffany: #4AB7BD; +$yellow:#FEC171; +$panGreen: #30B08F; + +// sidebar +$menuText:#bfcbd9; +$menuActiveText:#409EFF; +$subMenuActiveText:#f4f4f5; // https://github.com/ElemeFE/element/issues/12951 + +$menuBg:#304156; +$menuHover:#263445; + +$subMenuBg:#1f2d3d; +$subMenuHover:#001528; + +$sideBarWidth: 205px; + +// the :export directive is the magic sauce for webpack +// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass +:export { + menuText: $menuText; + menuActiveText: $menuActiveText; + subMenuActiveText: $subMenuActiveText; + menuBg: $menuBg; + menuHover: $menuHover; + subMenuBg: $subMenuBg; + subMenuHover: $subMenuHover; + sideBarWidth: $sideBarWidth; +} diff --git a/eladmin-web/src/components/Breadcrumb/index.vue b/eladmin-web/src/components/Breadcrumb/index.vue new file mode 100644 index 0000000..204ea59 --- /dev/null +++ b/eladmin-web/src/components/Breadcrumb/index.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/eladmin-web/src/components/Crud/CRUD.operation.vue b/eladmin-web/src/components/Crud/CRUD.operation.vue new file mode 100644 index 0000000..33d2077 --- /dev/null +++ b/eladmin-web/src/components/Crud/CRUD.operation.vue @@ -0,0 +1,268 @@ + + + + diff --git a/eladmin-web/src/components/Crud/Pagination.vue b/eladmin-web/src/components/Crud/Pagination.vue new file mode 100644 index 0000000..d4482fb --- /dev/null +++ b/eladmin-web/src/components/Crud/Pagination.vue @@ -0,0 +1,18 @@ + + + diff --git a/eladmin-web/src/components/Crud/RR.operation.vue b/eladmin-web/src/components/Crud/RR.operation.vue new file mode 100644 index 0000000..df2c138 --- /dev/null +++ b/eladmin-web/src/components/Crud/RR.operation.vue @@ -0,0 +1,20 @@ + + + diff --git a/eladmin-web/src/components/Crud/UD.operation.vue b/eladmin-web/src/components/Crud/UD.operation.vue new file mode 100644 index 0000000..221a4ef --- /dev/null +++ b/eladmin-web/src/components/Crud/UD.operation.vue @@ -0,0 +1,71 @@ + + diff --git a/eladmin-web/src/components/Crud/crud.js b/eladmin-web/src/components/Crud/crud.js new file mode 100644 index 0000000..b5bd398 --- /dev/null +++ b/eladmin-web/src/components/Crud/crud.js @@ -0,0 +1,864 @@ +import { initData, download } from '@/api/data' +import { parseTime, downloadFile } from '@/utils/index' +import Vue from 'vue' + +/** + * CRUD配置 + * @author moxun + * @param {*} options
+ * @return crud instance. + * @example + * 要使用多crud时,请在关联crud的组件处使用crud-tag进行标记,如: + */ +function CRUD(options) { + const defaultOptions = { + tag: 'default', + // id字段名 + idField: 'id', + // 标题 + title: '', + // 请求数据的url + url: '', + // 表格数据 + data: [], + // 选择项 + selections: [], + // 待查询的对象 + query: {}, + // 查询数据的参数 + params: {}, + // Form 表单 + form: {}, + // 重置表单 + defaultForm: () => {}, + // 排序规则,默认 id 降序, 支持多字段排序 ['id,desc', 'createTime,asc'] + sort: ['id,desc'], + // 等待时间 + time: 50, + // CRUD Method + crudMethod: { + add: (form) => {}, + del: (id) => {}, + edit: (form) => {}, + get: (id) => {} + }, + // 主页操作栏显示哪些按钮 + optShow: { + add: true, + edit: true, + del: true, + download: true, + reset: true + }, + // 自定义一些扩展属性 + props: {}, + // 在主页准备 + queryOnPresenterCreated: true, + // 调试开关 + debug: false + } + options = mergeOptions(defaultOptions, options) + const data = { + ...options, + // 记录数据状态 + dataStatus: {}, + status: { + add: CRUD.STATUS.NORMAL, + edit: CRUD.STATUS.NORMAL, + // 添加或编辑状态 + get cu() { + if (this.add === CRUD.STATUS.NORMAL && this.edit === CRUD.STATUS.NORMAL) { + return CRUD.STATUS.NORMAL + } else if (this.add === CRUD.STATUS.PREPARED || this.edit === CRUD.STATUS.PREPARED) { + return CRUD.STATUS.PREPARED + } else if (this.add === CRUD.STATUS.PROCESSING || this.edit === CRUD.STATUS.PROCESSING) { + return CRUD.STATUS.PROCESSING + } + throw new Error('wrong crud\'s cu status') + }, + // 标题 + get title() { + return this.add > CRUD.STATUS.NORMAL ? `新增${crud.title}` : this.edit > CRUD.STATUS.NORMAL ? `编辑${crud.title}` : crud.title + } + }, + msg: { + submit: '提交成功', + add: '新增成功', + edit: '编辑成功', + del: '删除成功' + }, + page: { + // 页码 + page: 0, + // 每页数据条数 + size: 10, + // 总数据条数 + total: 0 + }, + // 整体loading + loading: false, + // 导出的 Loading + downloadLoading: false, + // 删除的 Loading + delAllLoading: false + } + const methods = { + /** + * 通用的提示 + */ + submitSuccessNotify() { + crud.notify(crud.msg.submit, CRUD.NOTIFICATION_TYPE.SUCCESS) + }, + addSuccessNotify() { + crud.notify(crud.msg.add, CRUD.NOTIFICATION_TYPE.SUCCESS) + }, + editSuccessNotify() { + crud.notify(crud.msg.edit, CRUD.NOTIFICATION_TYPE.SUCCESS) + }, + delSuccessNotify() { + crud.notify(crud.msg.del, CRUD.NOTIFICATION_TYPE.SUCCESS) + }, + // 搜索 + toQuery() { + crud.page.page = 1 + crud.refresh() + }, + // 刷新 + refresh() { + if (!callVmHook(crud, CRUD.HOOK.beforeRefresh)) { + return + } + return new Promise((resolve, reject) => { + crud.loading = true + // 请求数据 + initData(crud.url, crud.getQueryParams()).then(data => { + const table = crud.getTable() + if (table && table.lazy) { // 懒加载子节点数据,清掉已加载的数据 + table.store.states.treeData = {} + table.store.states.lazyTreeNodeMap = {} + } + crud.page.total = data.totalElements + crud.data = data.content + crud.resetDataStatus() + // time 毫秒后显示表格 + setTimeout(() => { + crud.loading = false + callVmHook(crud, CRUD.HOOK.afterRefresh) + }, crud.time) + resolve(data) + }).catch(err => { + crud.loading = false + reject(err) + }) + }) + }, + /** + * 启动添加 + */ + toAdd() { + crud.resetForm() + if (!(callVmHook(crud, CRUD.HOOK.beforeToAdd, crud.form) && callVmHook(crud, CRUD.HOOK.beforeToCU, crud.form))) { + return + } + crud.status.add = CRUD.STATUS.PREPARED + callVmHook(crud, CRUD.HOOK.afterToAdd, crud.form) + callVmHook(crud, CRUD.HOOK.afterToCU, crud.form) + }, + /** + * 启动编辑 + * @param {*} data 数据项 + */ + toEdit(data) { + crud.resetForm(JSON.parse(JSON.stringify(data))) + if (!(callVmHook(crud, CRUD.HOOK.beforeToEdit, crud.form) && callVmHook(crud, CRUD.HOOK.beforeToCU, crud.form))) { + return + } + crud.status.edit = CRUD.STATUS.PREPARED + crud.getDataStatus(crud.getDataId(data)).edit = CRUD.STATUS.PREPARED + callVmHook(crud, CRUD.HOOK.afterToEdit, crud.form) + callVmHook(crud, CRUD.HOOK.afterToCU, crud.form) + }, + /** + * 启动删除 + * @param {*} data 数据项 + */ + toDelete(data) { + crud.getDataStatus(crud.getDataId(data)).delete = CRUD.STATUS.PREPARED + }, + /** + * 取消删除 + * @param {*} data 数据项 + */ + cancelDelete(data) { + if (!callVmHook(crud, CRUD.HOOK.beforeDeleteCancel, data)) { + return + } + crud.getDataStatus(crud.getDataId(data)).delete = CRUD.STATUS.NORMAL + callVmHook(crud, CRUD.HOOK.afterDeleteCancel, data) + }, + /** + * 取消新增/编辑 + */ + cancelCU() { + const addStatus = crud.status.add + const editStatus = crud.status.edit + if (addStatus === CRUD.STATUS.PREPARED) { + if (!callVmHook(crud, CRUD.HOOK.beforeAddCancel, crud.form)) { + return + } + crud.status.add = CRUD.STATUS.NORMAL + } + if (editStatus === CRUD.STATUS.PREPARED) { + if (!callVmHook(crud, CRUD.HOOK.beforeEditCancel, crud.form)) { + return + } + crud.status.edit = CRUD.STATUS.NORMAL + crud.getDataStatus(crud.getDataId(crud.form)).edit = CRUD.STATUS.NORMAL + } + crud.resetForm() + if (addStatus === CRUD.STATUS.PREPARED) { + callVmHook(crud, CRUD.HOOK.afterAddCancel, crud.form) + } + if (editStatus === CRUD.STATUS.PREPARED) { + callVmHook(crud, CRUD.HOOK.afterEditCancel, crud.form) + } + // 清除表单验证 + if (crud.findVM('form').$refs['form']) { + crud.findVM('form').$refs['form'].clearValidate() + } + }, + /** + * 提交新增/编辑 + */ + submitCU() { + if (!callVmHook(crud, CRUD.HOOK.beforeValidateCU)) { + return + } + crud.findVM('form').$refs['form'].validate(valid => { + if (!valid) { + return + } + if (!callVmHook(crud, CRUD.HOOK.afterValidateCU)) { + return + } + if (crud.status.add === CRUD.STATUS.PREPARED) { + crud.doAdd() + } else if (crud.status.edit === CRUD.STATUS.PREPARED) { + crud.doEdit() + } + }) + }, + /** + * 执行添加 + */ + doAdd() { + if (!callVmHook(crud, CRUD.HOOK.beforeSubmit)) { + return + } + crud.status.add = CRUD.STATUS.PROCESSING + crud.crudMethod.add(crud.form).then(() => { + crud.status.add = CRUD.STATUS.NORMAL + crud.resetForm() + crud.addSuccessNotify() + callVmHook(crud, CRUD.HOOK.afterSubmit) + crud.toQuery() + }).catch(() => { + crud.status.add = CRUD.STATUS.PREPARED + callVmHook(crud, CRUD.HOOK.afterAddError) + }) + }, + /** + * 执行编辑 + */ + doEdit() { + if (!callVmHook(crud, CRUD.HOOK.beforeSubmit)) { + return + } + crud.status.edit = CRUD.STATUS.PROCESSING + crud.crudMethod.edit(crud.form).then(() => { + crud.status.edit = CRUD.STATUS.NORMAL + crud.getDataStatus(crud.getDataId(crud.form)).edit = CRUD.STATUS.NORMAL + crud.editSuccessNotify() + crud.resetForm() + callVmHook(crud, CRUD.HOOK.afterSubmit) + crud.refresh() + }).catch(() => { + crud.status.edit = CRUD.STATUS.PREPARED + callVmHook(crud, CRUD.HOOK.afterEditError) + }) + }, + /** + * 执行删除 + * @param {*} data 数据项 + */ + doDelete(data) { + let delAll = false + let dataStatus + const ids = [] + if (data instanceof Array) { + delAll = true + data.forEach(val => { + ids.push(this.getDataId(val)) + }) + } else { + ids.push(this.getDataId(data)) + dataStatus = crud.getDataStatus(this.getDataId(data)) + } + if (!callVmHook(crud, CRUD.HOOK.beforeDelete, data)) { + return + } + if (!delAll) { + dataStatus.delete = CRUD.STATUS.PROCESSING + } + return crud.crudMethod.del(ids).then(() => { + if (delAll) { + crud.delAllLoading = false + } else dataStatus.delete = CRUD.STATUS.PREPARED + crud.dleChangePage(1) + crud.delSuccessNotify() + callVmHook(crud, CRUD.HOOK.afterDelete, data) + crud.refresh() + }).catch(() => { + if (delAll) { + crud.delAllLoading = false + } else dataStatus.delete = CRUD.STATUS.PREPARED + }) + }, + /** + * 通用导出 + */ + doExport() { + crud.downloadLoading = true + download(crud.url + '/download', crud.getQueryParams()).then(result => { + downloadFile(result, crud.title + '数据', 'xlsx') + crud.downloadLoading = false + }).catch(() => { + crud.downloadLoading = false + }) + }, + /** + * 获取查询参数 + */ + getQueryParams: function() { + // 清除参数无值的情况 + Object.keys(crud.query).length !== 0 && Object.keys(crud.query).forEach(item => { + if (crud.query[item] === null || crud.query[item] === '') crud.query[item] = undefined + }) + Object.keys(crud.params).length !== 0 && Object.keys(crud.params).forEach(item => { + if (crud.params[item] === null || crud.params[item] === '') crud.params[item] = undefined + }) + return { + page: crud.page.page - 1, + size: crud.page.size, + sort: crud.sort, + ...crud.query, + ...crud.params + } + }, + // 当前页改变 + pageChangeHandler(e) { + crud.page.page = e + crud.refresh() + }, + // 每页条数改变 + sizeChangeHandler(e) { + crud.page.size = e + crud.page.page = 1 + crud.refresh() + }, + // 预防删除第二页最后一条数据时,或者多选删除第二页的数据时,页码错误导致请求无数据 + dleChangePage(size) { + if (crud.data.length === size && crud.page.page !== 1) { + crud.page.page -= 1 + } + }, + // 选择改变 + selectionChangeHandler(val) { + crud.selections = val + }, + /** + * 重置查询参数 + * @param {Boolean} toQuery 重置后进行查询操作 + */ + resetQuery(toQuery = true) { + const defaultQuery = JSON.parse(JSON.stringify(crud.defaultQuery)) + const query = crud.query + Object.keys(query).forEach(key => { + query[key] = defaultQuery[key] + }) + // 重置参数 + this.params = {} + if (toQuery) { + crud.toQuery() + } + }, + /** + * 重置表单 + * @param {Array} data 数据 + */ + resetForm(data) { + const form = data || (typeof crud.defaultForm === 'object' ? JSON.parse(JSON.stringify(crud.defaultForm)) : crud.defaultForm.apply(crud.findVM('form'))) + const crudFrom = crud.form + for (const key in form) { + if (crudFrom.hasOwnProperty(key)) { + crudFrom[key] = form[key] + } else { + Vue.set(crudFrom, key, form[key]) + } + } + // add by ghl 2020-10-04 页面重复添加信息时,下拉框的校验会存在,需要找工取消 + if (crud.findVM('form').$refs['form']) { + crud.findVM('form').$refs['form'].clearValidate() + } + }, + /** + * 重置数据状态 + */ + resetDataStatus() { + const dataStatus = {} + function resetStatus(datas) { + datas.forEach(e => { + dataStatus[crud.getDataId(e)] = { + delete: 0, + edit: 0 + } + if (e.children) { + resetStatus(e.children) + } + }) + } + resetStatus(crud.data) + crud.dataStatus = dataStatus + }, + /** + * 获取数据状态 + * @param {Number | String} id 数据项id + */ + getDataStatus(id) { + return crud.dataStatus[id] + }, + /** + * 用于树形表格多选, 选中所有 + * @param selection + */ + selectAllChange(selection) { + // 如果选中的数目与请求到的数目相同就选中子节点,否则就清空选中 + if (selection && selection.length === crud.data.length) { + selection.forEach(val => { + crud.selectChange(selection, val) + }) + } else { + crud.getTable().clearSelection() + } + }, + /** + * 用于树形表格多选,单选的封装 + * @param selection + * @param row + */ + selectChange(selection, row) { + // 如果selection中存在row代表是选中,否则是取消选中 + if (selection.find(val => { return crud.getDataId(val) === crud.getDataId(row) })) { + if (row.children) { + row.children.forEach(val => { + crud.getTable().toggleRowSelection(val, true) + selection.push(val) + if (val.children) { + crud.selectChange(selection, val) + } + }) + } + } else { + crud.toggleRowSelection(selection, row) + } + }, + /** + * 切换选中状态 + * @param selection + * @param data + */ + toggleRowSelection(selection, data) { + if (data.children) { + data.children.forEach(val => { + selection.splice(selection.findIndex(item => this.getDataId(item) === this.getDataId(val)), 1) + crud.getTable().toggleRowSelection(val, false) + if (val.children) { + crud.toggleRowSelection(selection, val) + } + }) + } + }, + findVM(type) { + return crud.vms.find(vm => vm && vm.type === type).vm + }, + notify(title, type = CRUD.NOTIFICATION_TYPE.INFO) { + crud.vms[0].vm.$notify({ + title, + type, + duration: 2500 + }) + }, + updateProp(name, value) { + Vue.set(crud.props, name, value) + }, + getDataId(data) { + return data[this.idField] + }, + getTable() { + return this.findVM('presenter').$refs.table + }, + attchTable() { + const table = this.getTable() + this.updateProp('table', table) + const that = this + table.$on('expand-change', (row, expanded) => { + if (!expanded) { + return + } + const lazyTreeNodeMap = table.store.states.lazyTreeNodeMap + row.children = lazyTreeNodeMap[crud.getDataId(row)] + if (row.children) { + row.children.forEach(ele => { + const id = crud.getDataId(ele) + if (that.dataStatus[id] === undefined) { + that.dataStatus[id] = { + delete: 0, + edit: 0 + } + } + }) + } + }) + } + } + const crud = Object.assign({}, data) + // 可观测化 + Vue.observable(crud) + // 附加方法 + Object.assign(crud, methods) + // 记录初始默认的查询参数,后续重置查询时使用 + Object.assign(crud, { + defaultQuery: JSON.parse(JSON.stringify(data.query)), + // 预留4位存储:组件 主页、头部、分页、表单,调试查看也方便找 + vms: Array(4), + /** + * 注册组件实例 + * @param {String} type 类型 + * @param {*} vm 组件实例 + * @param {Number} index 该参数内部使用 + */ + registerVM(type, vm, index = -1) { + const vmObj = { + type, + vm: vm + } + if (index < 0) { + this.vms.push(vmObj) + return + } + if (index < 4) { // 内置预留vm数 + this.vms[index] = vmObj + return + } + this.vms.length = Math.max(this.vms.length, index) + this.vms.splice(index, 1, vmObj) + }, + /** + * 取消注册组件实例 + * @param {*} vm 组件实例 + */ + unregisterVM(type, vm) { + for (let i = this.vms.length - 1; i >= 0; i--) { + if (this.vms[i] === undefined) { + continue + } + if (this.vms[i].type === type && this.vms[i].vm === vm) { + if (i < 4) { // 内置预留vm数 + this.vms[i] = undefined + } else { + this.vms.splice(i, 1) + } + break + } + } + } + }) + // 冻结处理,需要扩展数据的话,使用crud.updateProp(name, value),以crud.props.name形式访问,这个是响应式的,可以做数据绑定 + Object.freeze(crud) + return crud +} + +// hook VM +function callVmHook(crud, hook) { + if (crud.debug) { + console.log('callVmHook: ' + hook) + } + const tagHook = crud.tag ? hook + '$' + crud.tag : null + let ret = true + const nargs = [crud] + for (let i = 2; i < arguments.length; ++i) { + nargs.push(arguments[i]) + } + // 有些组件扮演了多个角色,调用钩子时,需要去重 + const vmSet = new Set() + crud.vms.forEach(vm => vm && vmSet.add(vm.vm)) + vmSet.forEach(vm => { + if (vm[hook]) { + ret = vm[hook].apply(vm, nargs) !== false && ret + } + if (tagHook && vm[tagHook]) { + ret = vm[tagHook].apply(vm, nargs) !== false && ret + } + }) + return ret +} + +function mergeOptions(src, opts) { + const optsRet = { + ...src + } + for (const key in src) { + if (opts.hasOwnProperty(key)) { + optsRet[key] = opts[key] + } + } + return optsRet +} + +/** + * 查找crud + * @param {*} vm + * @param {string} tag + */ +function lookupCrud(vm, tag) { + tag = tag || vm.$attrs['crud-tag'] || 'default' + // function lookupCrud(vm, tag) { + if (vm.$crud) { + const ret = vm.$crud[tag] + if (ret) { + return ret + } + } + return vm.$parent ? lookupCrud(vm.$parent, tag) : undefined +} + +/** + * crud主页 + */ +function presenter(crud) { + if (crud) { + console.warn('[CRUD warn]: ' + 'please use $options.cruds() { return CRUD(...) or [CRUD(...), ...] }') + } + return { + data() { + // 在data中返回crud,是为了将crud与当前实例关联,组件观测crud相关属性变化 + return { + crud: this.crud + } + }, + beforeCreate() { + this.$crud = this.$crud || {} + let cruds = this.$options.cruds instanceof Function ? this.$options.cruds() : crud + if (!(cruds instanceof Array)) { + cruds = [cruds] + } + cruds.forEach(ele => { + if (this.$crud[ele.tag]) { + console.error('[CRUD error]: ' + 'crud with tag [' + ele.tag + ' is already exist') + } + this.$crud[ele.tag] = ele + ele.registerVM('presenter', this, 0) + }) + this.crud = this.$crud['defalut'] || cruds[0] + }, + methods: { + parseTime + }, + created() { + for (const k in this.$crud) { + if (this.$crud[k].queryOnPresenterCreated) { + this.$crud[k].toQuery() + } + } + }, + destroyed() { + for (const k in this.$crud) { + this.$crud[k].unregisterVM('presenter', this) + } + }, + mounted() { + // 如果table未实例化(例如使用了v-if),请稍后在适当时机crud.attchTable刷新table信息 + if (this.$refs.table !== undefined) { + this.crud.attchTable() + } + } + } +} + +/** + * 头部 + */ +function header() { + return { + data() { + return { + crud: this.crud, + query: this.crud.query + } + }, + beforeCreate() { + this.crud = lookupCrud(this) + this.crud.registerVM('header', this, 1) + }, + destroyed() { + this.crud.unregisterVM('header', this) + } + } +} + +/** + * 分页 + */ +function pagination() { + return { + data() { + return { + crud: this.crud, + page: this.crud.page + } + }, + beforeCreate() { + this.crud = lookupCrud(this) + this.crud.registerVM('pagination', this, 2) + }, + destroyed() { + this.crud.unregisterVM('pagination', this) + } + } +} + +/** + * 表单 + */ +function form(defaultForm) { + return { + data() { + return { + crud: this.crud, + form: this.crud.form + } + }, + beforeCreate() { + this.crud = lookupCrud(this) + this.crud.registerVM('form', this, 3) + }, + created() { + this.crud.defaultForm = defaultForm + this.crud.resetForm() + }, + destroyed() { + this.crud.unregisterVM('form', this) + } + } +} + +/** + * crud + */ +function crud(options = {}) { + const defaultOptions = { + type: undefined + } + options = mergeOptions(defaultOptions, options) + return { + data() { + return { + crud: this.crud + } + }, + beforeCreate() { + this.crud = lookupCrud(this) + this.crud.registerVM(options.type, this) + }, + destroyed() { + this.crud.unregisterVM(options.type, this) + } + } +} + +/** + * CRUD钩子 + */ +CRUD.HOOK = { + /** 刷新 - 之前 */ + beforeRefresh: 'beforeCrudRefresh', + /** 刷新 - 之后 */ + afterRefresh: 'afterCrudRefresh', + /** 删除 - 之前 */ + beforeDelete: 'beforeCrudDelete', + /** 删除 - 之后 */ + afterDelete: 'afterCrudDelete', + /** 删除取消 - 之前 */ + beforeDeleteCancel: 'beforeCrudDeleteCancel', + /** 删除取消 - 之后 */ + afterDeleteCancel: 'afterCrudDeleteCancel', + /** 新建 - 之前 */ + beforeToAdd: 'beforeCrudToAdd', + /** 新建 - 之后 */ + afterToAdd: 'afterCrudToAdd', + /** 编辑 - 之前 */ + beforeToEdit: 'beforeCrudToEdit', + /** 编辑 - 之后 */ + afterToEdit: 'afterCrudToEdit', + /** 开始 "新建/编辑" - 之前 */ + beforeToCU: 'beforeCrudToCU', + /** 开始 "新建/编辑" - 之后 */ + afterToCU: 'afterCrudToCU', + /** "新建/编辑" 验证 - 之前 */ + beforeValidateCU: 'beforeCrudValidateCU', + /** "新建/编辑" 验证 - 之后 */ + afterValidateCU: 'afterCrudValidateCU', + /** 添加取消 - 之前 */ + beforeAddCancel: 'beforeCrudAddCancel', + /** 添加取消 - 之后 */ + afterAddCancel: 'afterCrudAddCancel', + /** 编辑取消 - 之前 */ + beforeEditCancel: 'beforeCrudEditCancel', + /** 编辑取消 - 之后 */ + afterEditCancel: 'afterCrudEditCancel', + /** 提交 - 之前 */ + beforeSubmit: 'beforeCrudSubmitCU', + /** 提交 - 之后 */ + afterSubmit: 'afterCrudSubmitCU', + afterAddError: 'afterCrudAddError', + afterEditError: 'afterCrudEditError' +} + +/** + * CRUD状态 + */ +CRUD.STATUS = { + NORMAL: 0, + PREPARED: 1, + PROCESSING: 2 +} + +/** + * CRUD通知类型 + */ +CRUD.NOTIFICATION_TYPE = { + SUCCESS: 'success', + WARNING: 'warning', + INFO: 'info', + ERROR: 'error' +} + +export default CRUD + +export { + presenter, + header, + form, + pagination, + crud +} diff --git a/eladmin-web/src/components/DateRangePicker/index.vue b/eladmin-web/src/components/DateRangePicker/index.vue new file mode 100644 index 0000000..8feea56 --- /dev/null +++ b/eladmin-web/src/components/DateRangePicker/index.vue @@ -0,0 +1,45 @@ + diff --git a/eladmin-web/src/components/Dict/Dict.js b/eladmin-web/src/components/Dict/Dict.js new file mode 100644 index 0000000..48554de --- /dev/null +++ b/eladmin-web/src/components/Dict/Dict.js @@ -0,0 +1,29 @@ +import Vue from 'vue' +import { get as getDictDetail } from '@/api/system/dictDetail' + +export default class Dict { + constructor(dict) { + this.dict = dict + } + + async init(names, completeCallback) { + if (names === undefined || name === null) { + throw new Error('need Dict names') + } + const ps = [] + names.forEach(n => { + Vue.set(this.dict.dict, n, {}) + Vue.set(this.dict.label, n, {}) + Vue.set(this.dict, n, []) + ps.push(getDictDetail(n).then(data => { + this.dict[n].splice(0, 0, ...data.content) + data.content.forEach(d => { + Vue.set(this.dict.dict[n], d.value, d) + Vue.set(this.dict.label[n], d.value, d.label) + }) + })) + }) + await Promise.all(ps) + completeCallback() + } +} diff --git a/eladmin-web/src/components/Dict/index.js b/eladmin-web/src/components/Dict/index.js new file mode 100644 index 0000000..0952f43 --- /dev/null +++ b/eladmin-web/src/components/Dict/index.js @@ -0,0 +1,29 @@ +import Dict from './Dict' + +const install = function(Vue) { + Vue.mixin({ + data() { + if (this.$options.dicts instanceof Array) { + const dict = { + dict: {}, + label: {} + } + return { + dict + } + } + return {} + }, + created() { + if (this.$options.dicts instanceof Array) { + new Dict(this.dict).init(this.$options.dicts, () => { + this.$nextTick(() => { + this.$emit('dictReady') + }) + }) + } + } + }) +} + +export default { install } diff --git a/eladmin-web/src/components/Doc/index.vue b/eladmin-web/src/components/Doc/index.vue new file mode 100644 index 0000000..8c59930 --- /dev/null +++ b/eladmin-web/src/components/Doc/index.vue @@ -0,0 +1,16 @@ + + + diff --git a/eladmin-web/src/components/Echarts/BarChart.vue b/eladmin-web/src/components/Echarts/BarChart.vue new file mode 100644 index 0000000..fa265ef --- /dev/null +++ b/eladmin-web/src/components/Echarts/BarChart.vue @@ -0,0 +1,106 @@ + + + diff --git a/eladmin-web/src/components/Echarts/Category.vue b/eladmin-web/src/components/Echarts/Category.vue new file mode 100644 index 0000000..5859114 --- /dev/null +++ b/eladmin-web/src/components/Echarts/Category.vue @@ -0,0 +1,438 @@ + + + diff --git a/eladmin-web/src/components/Echarts/Funnel.vue b/eladmin-web/src/components/Echarts/Funnel.vue new file mode 100644 index 0000000..380b373 --- /dev/null +++ b/eladmin-web/src/components/Echarts/Funnel.vue @@ -0,0 +1,120 @@ + + + diff --git a/eladmin-web/src/components/Echarts/Gauge.vue b/eladmin-web/src/components/Echarts/Gauge.vue new file mode 100644 index 0000000..40ce775 --- /dev/null +++ b/eladmin-web/src/components/Echarts/Gauge.vue @@ -0,0 +1,74 @@ + + + diff --git a/eladmin-web/src/components/Echarts/Graph.vue b/eladmin-web/src/components/Echarts/Graph.vue new file mode 100644 index 0000000..a5a88b3 --- /dev/null +++ b/eladmin-web/src/components/Echarts/Graph.vue @@ -0,0 +1,101 @@ + + + diff --git a/eladmin-web/src/components/Echarts/HeatMap.vue b/eladmin-web/src/components/Echarts/HeatMap.vue new file mode 100644 index 0000000..6606278 --- /dev/null +++ b/eladmin-web/src/components/Echarts/HeatMap.vue @@ -0,0 +1,105 @@ + + + diff --git a/eladmin-web/src/components/Echarts/PieChart.vue b/eladmin-web/src/components/Echarts/PieChart.vue new file mode 100644 index 0000000..ff1bc52 --- /dev/null +++ b/eladmin-web/src/components/Echarts/PieChart.vue @@ -0,0 +1,84 @@ + + + diff --git a/eladmin-web/src/components/Echarts/Point.vue b/eladmin-web/src/components/Echarts/Point.vue new file mode 100644 index 0000000..5a6c777 --- /dev/null +++ b/eladmin-web/src/components/Echarts/Point.vue @@ -0,0 +1,149 @@ + + + diff --git a/eladmin-web/src/components/Echarts/RadarChart.vue b/eladmin-web/src/components/Echarts/RadarChart.vue new file mode 100644 index 0000000..de70e52 --- /dev/null +++ b/eladmin-web/src/components/Echarts/RadarChart.vue @@ -0,0 +1,120 @@ + + + diff --git a/eladmin-web/src/components/Echarts/Rich.vue b/eladmin-web/src/components/Echarts/Rich.vue new file mode 100644 index 0000000..1cf6bf2 --- /dev/null +++ b/eladmin-web/src/components/Echarts/Rich.vue @@ -0,0 +1,149 @@ + + + diff --git a/eladmin-web/src/components/Echarts/Sankey.vue b/eladmin-web/src/components/Echarts/Sankey.vue new file mode 100644 index 0000000..49968c4 --- /dev/null +++ b/eladmin-web/src/components/Echarts/Sankey.vue @@ -0,0 +1,100 @@ + + + diff --git a/eladmin-web/src/components/Echarts/Scatter.vue b/eladmin-web/src/components/Echarts/Scatter.vue new file mode 100644 index 0000000..b776efb --- /dev/null +++ b/eladmin-web/src/components/Echarts/Scatter.vue @@ -0,0 +1,143 @@ + + + diff --git a/eladmin-web/src/components/Echarts/Sunburst.vue b/eladmin-web/src/components/Echarts/Sunburst.vue new file mode 100644 index 0000000..fafc1a8 --- /dev/null +++ b/eladmin-web/src/components/Echarts/Sunburst.vue @@ -0,0 +1,107 @@ + + + diff --git a/eladmin-web/src/components/Echarts/ThemeRiver.vue b/eladmin-web/src/components/Echarts/ThemeRiver.vue new file mode 100644 index 0000000..967fc05 --- /dev/null +++ b/eladmin-web/src/components/Echarts/ThemeRiver.vue @@ -0,0 +1,148 @@ + + + diff --git a/eladmin-web/src/components/GithubCorner/index.vue b/eladmin-web/src/components/GithubCorner/index.vue new file mode 100644 index 0000000..fd77324 --- /dev/null +++ b/eladmin-web/src/components/GithubCorner/index.vue @@ -0,0 +1,54 @@ + + + diff --git a/eladmin-web/src/components/Hamburger/index.vue b/eladmin-web/src/components/Hamburger/index.vue new file mode 100644 index 0000000..368b002 --- /dev/null +++ b/eladmin-web/src/components/Hamburger/index.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/eladmin-web/src/components/HeaderSearch/index.vue b/eladmin-web/src/components/HeaderSearch/index.vue new file mode 100644 index 0000000..c713efc --- /dev/null +++ b/eladmin-web/src/components/HeaderSearch/index.vue @@ -0,0 +1,188 @@ + + + + + diff --git a/eladmin-web/src/components/IconSelect/index.vue b/eladmin-web/src/components/IconSelect/index.vue new file mode 100644 index 0000000..b0ec9fa --- /dev/null +++ b/eladmin-web/src/components/IconSelect/index.vue @@ -0,0 +1,68 @@ + + + + + + diff --git a/eladmin-web/src/components/IconSelect/requireIcons.js b/eladmin-web/src/components/IconSelect/requireIcons.js new file mode 100644 index 0000000..99e5c54 --- /dev/null +++ b/eladmin-web/src/components/IconSelect/requireIcons.js @@ -0,0 +1,11 @@ + +const req = require.context('../../assets/icons/svg', false, /\.svg$/) +const requireAll = requireContext => requireContext.keys() + +const re = /\.\/(.*)\.svg/ + +const icons = requireAll(req).map(i => { + return i.match(re)[1] +}) + +export default icons diff --git a/eladmin-web/src/components/Iframe/index.vue b/eladmin-web/src/components/Iframe/index.vue new file mode 100644 index 0000000..cdd612b --- /dev/null +++ b/eladmin-web/src/components/Iframe/index.vue @@ -0,0 +1,30 @@ +