From 5b04a92a559f8a04f42935fd75552544056cf40e Mon Sep 17 00:00:00 2001 From: dute7liang <383200134@qq.com> Date: Tue, 19 Dec 2023 22:25:27 +0800 Subject: [PATCH] init --- .editorconfig | 21 + .gitignore | 44 + bashi-admin/pom.xml | 105 ++ .../main/java/com/bashi/BaShiApplication.java | 24 + .../com/bashi/BaShiServletInitializer.java | 18 + .../com/bashi/com/UserDetailsServiceImpl.java | 105 ++ .../controller/common/CaptchaController.java | 121 ++ .../controller/common/CommonController.java | 107 ++ .../controller/monitor/CacheController.java | 50 + .../monitor/SysLogininforController.java | 62 + .../monitor/SysOperlogController.java | 62 + .../monitor/SysUserOnlineController.java | 91 ++ .../system/SysConfigController.java | 128 ++ .../controller/system/SysDeptController.java | 159 +++ .../system/SysDictDataController.java | 113 ++ .../system/SysDictTypeController.java | 124 ++ .../controller/system/SysLoginController.java | 93 ++ .../controller/system/SysMenuController.java | 153 +++ .../system/SysNoticeController.java | 89 ++ .../controller/system/SysPostController.java | 128 ++ .../system/SysProfileController.java | 137 +++ .../controller/system/SysRoleController.java | 174 +++ .../controller/system/SysUserController.java | 180 +++ .../META-INF/spring-devtools.properties | 1 + .../src/main/resources/application-dev.yml | 112 ++ .../src/main/resources/application-prod.yml | 112 ++ .../src/main/resources/application.yml | 340 ++++++ bashi-admin/src/main/resources/banner.txt | 2 + .../main/resources/i18n/messages.properties | 36 + bashi-admin/src/main/resources/logback.xml | 71 ++ .../src/test/java/com/bashi/BaseTest.java | 10 + bashi-common/pom.xml | 164 +++ .../bashi/common/annotation/DataScope.java | 33 + .../bashi/common/annotation/DataSource.java | 28 + .../com/bashi/common/annotation/Excel.java | 165 +++ .../com/bashi/common/annotation/Excels.java | 18 + .../java/com/bashi/common/annotation/Log.java | 52 + .../bashi/common/annotation/RepeatSubmit.java | 23 + .../java/com/bashi/common/com/Condition.java | 32 + .../java/com/bashi/common/com/PageParams.java | 39 + .../com/bashi/common/config/BsConfig.java | 76 ++ .../com/bashi/common/constant/Constants.java | 139 +++ .../bashi/common/constant/DateConstant.java | 13 + .../bashi/common/constant/GenConstants.java | 114 ++ .../common/constant/ScheduleConstants.java | 50 + .../bashi/common/constant/UserConstants.java | 63 + .../core/controller/BaseController.java | 70 ++ .../bashi/common/core/domain/AjaxResult.java | 134 +++ .../bashi/common/core/domain/BaseEntity.java | 46 + .../bashi/common/core/domain/TreeEntity.java | 38 + .../bashi/common/core/domain/TreeSelect.java | 50 + .../common/core/domain/entity/Customer.java | 115 ++ .../common/core/domain/entity/SysDept.java | 99 ++ .../core/domain/entity/SysDictData.java | 107 ++ .../core/domain/entity/SysDictType.java | 80 ++ .../common/core/domain/entity/SysMenu.java | 108 ++ .../common/core/domain/entity/SysRole.java | 126 ++ .../common/core/domain/entity/SysUser.java | 168 +++ .../common/core/domain/model/LoginBody.java | 37 + .../core/domain/model/LoginPhoneBody.java | 39 + .../common/core/domain/model/LoginUser.java | 142 +++ .../cache/MybatisPlusRedisCache.java | 102 ++ .../core/mybatisplus/core/BaseMapperPlus.java | 18 + .../core/mybatisplus/methods/InsertAll.java | 52 + .../com/bashi/common/core/page/PagePlus.java | 156 +++ .../bashi/common/core/page/TableDataInfo.java | 60 + .../bashi/common/core/redis/RedisCache.java | 223 ++++ .../bashi/common/enums/BusinessStatus.java | 20 + .../com/bashi/common/enums/BusinessType.java | 59 + .../bashi/common/enums/DataSourceType.java | 25 + .../com/bashi/common/enums/HttpMethod.java | 36 + .../com/bashi/common/enums/OperatorType.java | 24 + .../com/bashi/common/enums/UserStatus.java | 30 + .../bashi/common/exception/BaseException.java | 97 ++ .../common/exception/CustomException.java | 45 + .../common/exception/DemoModeException.java | 15 + .../bashi/common/exception/UtilException.java | 26 + .../common/exception/file/FileException.java | 19 + .../FileNameLengthLimitExceededException.java | 16 + .../file/FileSizeLimitExceededException.java | 16 + .../file/InvalidExtensionException.java | 81 ++ .../common/exception/job/TaskException.java | 34 + .../exception/user/CaptchaException.java | 16 + .../user/CaptchaExpireException.java | 16 + .../common/exception/user/UserException.java | 18 + .../user/UserPasswordNotMatchException.java | 16 + .../bashi/common/filter/RepeatableFilter.java | 48 + .../filter/RepeatedlyRequestWrapper.java | 77 ++ .../com/bashi/common/filter/XssFilter.java | 93 ++ .../filter/XssHttpServletRequestWrapper.java | 108 ++ .../bashi/common/utils/BeanContextUtils.java | 29 + .../bashi/common/utils/BeanConvertUtil.java | 102 ++ .../com/bashi/common/utils/DateUtils.java | 155 +++ .../com/bashi/common/utils/DictUtils.java | 187 +++ .../com/bashi/common/utils/JsonUtils.java | 101 ++ .../com/bashi/common/utils/MessageUtils.java | 26 + .../com/bashi/common/utils/PageUtils.java | 155 +++ .../com/bashi/common/utils/SecurityUtils.java | 96 ++ .../com/bashi/common/utils/ServletUtils.java | 130 ++ .../java/com/bashi/common/utils/Threads.java | 99 ++ .../common/utils/file/FileTypeUtils.java | 76 ++ .../common/utils/file/FileUploadUtils.java | 239 ++++ .../bashi/common/utils/file/FileUtils.java | 125 ++ .../bashi/common/utils/file/ImageUtils.java | 102 ++ .../common/utils/file/MimeTypeUtils.java | 59 + .../bashi/common/utils/ip/AddressUtils.java | 55 + .../com/bashi/common/utils/poi/ExcelUtil.java | 1072 +++++++++++++++++ .../common/utils/reflect/ReflectUtils.java | 54 + .../common/utils/spring/SpringUtils.java | 65 + .../com/bashi/common/utils/sql/SqlUtil.java | 37 + bashi-dk/pom.xml | 51 + .../dk/controller/BorrowStatusController.java | 103 ++ .../DkAgreementSettingController.java | 43 + .../dk/controller/DkBorrowController.java | 124 ++ .../dk/controller/DkCustomerController.java | 88 ++ .../controller/DkCustomerInfoController.java | 64 + .../controller/DkHomeSettingController.java | 51 + .../controller/DkLoansSettingController.java | 50 + .../controller/app/AppBorrowController.java | 88 ++ .../controller/app/AppCustomerController.java | 50 + .../app/AppCustomerOpenController.java | 89 ++ .../dk/controller/app/AppHomeController.java | 65 + .../controller/app/AppSettingController.java | 74 ++ .../dk/controller/app/LoginV2Controller.java | 33 + .../dk/controller/app/V2CommonController.java | 44 + .../com/bashi/dk/domain/AgreementSetting.java | 55 + .../main/java/com/bashi/dk/domain/Borrow.java | 210 ++++ .../java/com/bashi/dk/domain/BorrowLog.java | 21 + .../com/bashi/dk/domain/BorrowStatus.java | 59 + .../com/bashi/dk/domain/CustomerInfo.java | 162 +++ .../java/com/bashi/dk/domain/HomeSetting.java | 42 + .../com/bashi/dk/domain/LoansSetting.java | 61 + .../dto/admin/req/BorrowUpdateStatusReq.java | 13 + .../dto/admin/req/UpdatePwdCustomerReq.java | 9 + .../bashi/dk/dto/admin/resp/BorrowResp.java | 10 + .../dk/dto/admin/resp/CustomerAdminResp.java | 11 + .../dk/dto/admin/resp/CustomerExportVo.java | 116 ++ .../bashi/dk/dto/admin/resp/CustomerInfo.java | 77 ++ .../bashi/dk/dto/app/req/BorrowStartReq.java | 25 + .../com/bashi/dk/dto/app/req/CalLoanReq.java | 20 + .../dk/dto/app/req/CustomerRegisterReq.java | 29 + .../dk/dto/app/req/UpdatePwdOpenReq.java | 17 + .../com/bashi/dk/dto/app/resp/BorrowInfo.java | 14 + .../bashi/dk/dto/app/resp/BorrowStepResp.java | 24 + .../bashi/dk/dto/app/resp/CalLoanResp.java | 28 + .../dk/dto/app/resp/LoanProcessResp.java | 22 + .../com/bashi/dk/dto/app/resp/LoanUser.java | 26 + .../bashi/dk/dto/app/resp/LoansSettingVO.java | 53 + .../com/bashi/dk/enums/BankTypeEnums.java | 25 + .../java/com/bashi/dk/kit/CalLoanManager.java | 42 + .../java/com/bashi/dk/kit/DkLoginKit.java | 80 ++ .../bashi/dk/manager/FileUploadManager.java | 55 + .../com/bashi/dk/manager/FileUploadRes.java | 10 + .../dk/mapper/AgreementSettingMapper.java | 7 + .../com/bashi/dk/mapper/BorrowLogMapper.java | 7 + .../com/bashi/dk/mapper/BorrowMapper.java | 11 + .../bashi/dk/mapper/BorrowStatusMapper.java | 7 + .../bashi/dk/mapper/CustomerInfoMapper.java | 7 + .../com/bashi/dk/mapper/CustomerMapper.java | 23 + .../bashi/dk/mapper/HomeSettingMapper.java | 7 + .../bashi/dk/mapper/LoansSettingMapper.java | 7 + .../java/com/bashi/dk/oss/ali/AliOssKit.java | 79 ++ .../java/com/bashi/dk/oss/ali/CosConfig.java | 23 + .../java/com/bashi/dk/oss/ali/CosKit.java | 105 ++ .../java/com/bashi/dk/oss/ali/OssConfig.java | 24 + .../dk/service/AgreementSettingService.java | 8 + .../com/bashi/dk/service/BorrowService.java | 32 + .../bashi/dk/service/BorrowStatusService.java | 7 + .../bashi/dk/service/CustomerInfoService.java | 12 + .../com/bashi/dk/service/CustomerService.java | 26 + .../bashi/dk/service/HomeSettingService.java | 8 + .../bashi/dk/service/LoansSettingService.java | 8 + .../impl/AgreementSettingServiceImpl.java | 18 + .../dk/service/impl/BorrowServiceImpl.java | 268 +++++ .../service/impl/BorrowStatusServiceImpl.java | 11 + .../service/impl/CustomerInfoServiceImpl.java | 86 ++ .../dk/service/impl/CustomerServiceImpl.java | 93 ++ .../service/impl/HomeSettingServiceImpl.java | 16 + .../service/impl/LoansSettingServiceImpl.java | 16 + .../com/bashi/dk/util/ACMLoanCalculator.java | 62 + .../bashi/dk/util/ACPIMLoanCalculator.java | 59 + .../com/bashi/dk/util/ContentReplaceUtil.java | 46 + .../com/bashi/dk/util/ILoanCalculator.java | 21 + .../java/com/bashi/dk/util/ImageUtil.java | 154 +++ .../src/main/java/com/bashi/dk/util/Loan.java | 115 ++ .../java/com/bashi/dk/util/LoanByMonth.java | 83 ++ .../bashi/dk/util/LoanCalculatorAdapter.java | 17 + .../com/bashi/dk/util/LoanCalculatorTest.java | 57 + .../main/java/com/bashi/dk/util/LoanUtil.java | 33 + .../java/com/bashi/dk/util/MoneyUtil.java | 141 +++ .../com/bashi/dk/util/OrderTradeNoUtil.java | 27 + .../com/bashi/dk/util/PhoneRandomUtil.java | 21 + .../java/com/bashi/dk/util/SnowFlake.java | 100 ++ .../main/resources/mapper/BorrowMapper.xml | 18 + .../main/resources/mapper/CustomerMapper.xml | 51 + bashi-framework/pom.xml | 91 ++ .../com/bashi/framework/app/AppTypeEnums.java | 37 + .../bashi/framework/app/AppTypeFactory.java | 24 + .../framework/aspectj/DataScopeAspect.java | 175 +++ .../framework/aspectj/DataSourceAspect.java | 62 + .../bashi/framework/aspectj/LogAspect.java | 240 ++++ .../captcha/UnsignedMathGenerator.java | 85 ++ .../framework/config/AdminServerConfig.java | 63 + .../framework/config/ApplicationConfig.java | 26 + .../bashi/framework/config/AsyncConfig.java | 51 + .../bashi/framework/config/CaptchaConfig.java | 55 + .../bashi/framework/config/DruidConfig.java | 66 + .../bashi/framework/config/FeignConfig.java | 57 + .../bashi/framework/config/FilterConfig.java | 54 + .../bashi/framework/config/JacksonConfig.java | 50 + .../framework/config/MybatisPlusConfig.java | 108 ++ .../bashi/framework/config/RedisConfig.java | 110 ++ .../framework/config/ResourcesConfig.java | 62 + .../framework/config/SecurityConfig.java | 172 +++ .../bashi/framework/config/ServerConfig.java | 33 + .../SmsCodeAuthenticationSecurityConfig.java | 47 + .../bashi/framework/config/SwaggerConfig.java | 108 ++ .../framework/config/ThreadPoolConfig.java | 70 ++ .../framework/config/ValidatorConfig.java | 31 + .../framework/config/WebSocketConfig.java | 14 + .../framework/config/WebSocketServer.java | 146 +++ .../framework/config/WebSocketServerDemo.java | 143 +++ .../config/properties/CaptchaProperties.java | 41 + .../config/properties/RedissonProperties.java | 100 ++ .../config/properties/SwaggerProperties.java | 63 + .../properties/ThreadPoolProperties.java | 47 + .../config/properties/TokenProperties.java | 31 + .../config/properties/XssProperties.java | 32 + .../bashi/framework/constant/CodeType.java | 17 + .../framework/dto/WebSocketMessageDTO.java | 10 + .../framework/dto/WebSocketMessageType.java | 5 + .../interceptor/RepeatSubmitInterceptor.java | 56 + .../impl/SameUrlDataInterceptor.java | 133 ++ .../framework/manager/ShutdownManager.java | 41 + .../CreateAndUpdateMetaObjectHandler.java | 45 + .../filter/JwtAuthenticationTokenFilter.java | 52 + .../handle/AuthenticationEntryPointImpl.java | 35 + .../handle/LogoutSuccessHandlerImpl.java | 53 + .../security/sms/IDuteUserDetailsService.java | 17 + .../security/sms/LoginTypeEnums.java | 37 + .../sms/SmsAuthenticationFailureHandler.java | 41 + .../security/sms/SmsAuthenticationFilter.java | 94 ++ .../sms/SmsAuthenticationProvider.java | 60 + .../sms/SmsAuthenticationSuccessHandler.java | 69 ++ .../security/sms/SmsAuthenticationToken.java | 74 ++ .../com/bashi/framework/util/AgentUtils.java | 83 ++ .../web/exception/GlobalExceptionHandler.java | 117 ++ .../framework/web/service/AsyncService.java | 100 ++ .../framework/web/service/CodeService.java | 56 + .../web/service/CustomerService.java | 10 + .../web/service/PermissionService.java | 170 +++ .../web/service/RegisterEventService.java | 13 + .../web/service/SysLoginService.java | 106 ++ .../web/service/SysPermissionService.java | 66 + .../framework/web/service/TokenService.java | 194 +++ bashi-generator/pom.xml | 34 + .../com/bashi/generator/config/GenConfig.java | 73 ++ .../generator/controller/GenController.java | 204 ++++ .../com/bashi/generator/domain/GenTable.java | 237 ++++ .../generator/domain/GenTableColumn.java | 249 ++++ .../mapper/GenTableColumnMapper.java | 22 + .../generator/mapper/GenTableMapper.java | 69 ++ .../service/GenTableColumnServiceImpl.java | 65 + .../service/GenTableServiceImpl.java | 457 +++++++ .../service/IGenTableColumnService.java | 45 + .../generator/service/IGenTableService.java | 130 ++ .../com/bashi/generator/util/GenUtils.java | 259 ++++ .../generator/util/VelocityInitializer.java | 35 + .../bashi/generator/util/VelocityUtils.java | 385 ++++++ .../src/main/resources/generator.yml | 10 + .../mapper/generator/GenTableColumnMapper.xml | 38 + .../mapper/generator/GenTableMapper.xml | 174 +++ .../src/main/resources/vm/java/addBo.java.vm | 45 + .../main/resources/vm/java/controller.java.vm | 121 ++ .../src/main/resources/vm/java/domain.java.vm | 50 + .../src/main/resources/vm/java/editBo.java.vm | 44 + .../src/main/resources/vm/java/mapper.java.vm | 16 + .../main/resources/vm/java/queryBo.java.vm | 54 + .../main/resources/vm/java/service.java.vm | 13 + .../resources/vm/java/serviceImpl.java.vm | 18 + .../main/resources/vm/java/sub-domain.java.vm | 73 ++ .../src/main/resources/vm/java/vo.java.vm | 50 + .../src/main/resources/vm/js/api.js.vm | 53 + .../src/main/resources/vm/sql/sql.vm | 22 + .../main/resources/vm/vue/index-tree.vue.vm | 546 +++++++++ .../src/main/resources/vm/vue/index.vue.vm | 653 ++++++++++ .../src/main/resources/vm/xml/mapper.xml.vm | 14 + bashi-quartz/pom.xml | 40 + .../bashi/quartz/config/ScheduleConfig.java | 13 + .../quartz/controller/SysJobController.java | 144 +++ .../controller/SysJobLogController.java | 85 ++ .../java/com/bashi/quartz/domain/SysJob.java | 134 +++ .../com/bashi/quartz/domain/SysJobLog.java | 78 ++ .../bashi/quartz/mapper/SysJobLogMapper.java | 13 + .../com/bashi/quartz/mapper/SysJobMapper.java | 13 + .../quartz/service/ISysJobLogService.java | 62 + .../bashi/quartz/service/ISysJobService.java | 106 ++ .../service/impl/SysJobLogServiceImpl.java | 114 ++ .../service/impl/SysJobServiceImpl.java | 246 ++++ .../java/com/bashi/quartz/task/RyTask.java | 29 + .../bashi/quartz/util/AbstractQuartzJob.java | 109 ++ .../java/com/bashi/quartz/util/CronUtils.java | 63 + .../com/bashi/quartz/util/JobInvokeUtil.java | 184 +++ .../QuartzDisallowConcurrentExecution.java | 21 + .../bashi/quartz/util/QuartzJobExecution.java | 19 + .../com/bashi/quartz/util/ScheduleUtils.java | 113 ++ .../mapper/quartz/SysJobLogMapper.xml | 18 + .../resources/mapper/quartz/SysJobMapper.xml | 23 + bashi-system/pom.xml | 28 + .../com/bashi/system/domain/SysConfig.java | 105 ++ .../bashi/system/domain/SysLogininfor.java | 94 ++ .../com/bashi/system/domain/SysNotice.java | 93 ++ .../com/bashi/system/domain/SysOperLog.java | 142 +++ .../java/com/bashi/system/domain/SysPost.java | 110 ++ .../com/bashi/system/domain/SysRoleDept.java | 29 + .../com/bashi/system/domain/SysRoleMenu.java | 29 + .../bashi/system/domain/SysUserOnline.java | 56 + .../com/bashi/system/domain/SysUserPost.java | 29 + .../com/bashi/system/domain/SysUserRole.java | 29 + .../com/bashi/system/domain/vo/MetaVo.java | 42 + .../com/bashi/system/domain/vo/RouterVo.java | 59 + .../bashi/system/mapper/SysConfigMapper.java | 13 + .../bashi/system/mapper/SysDeptMapper.java | 41 + .../system/mapper/SysDictDataMapper.java | 23 + .../system/mapper/SysDictTypeMapper.java | 13 + .../system/mapper/SysLogininforMapper.java | 13 + .../bashi/system/mapper/SysMenuMapper.java | 63 + .../bashi/system/mapper/SysNoticeMapper.java | 13 + .../bashi/system/mapper/SysOperLogMapper.java | 13 + .../bashi/system/mapper/SysPostMapper.java | 31 + .../system/mapper/SysRoleDeptMapper.java | 13 + .../bashi/system/mapper/SysRoleMapper.java | 52 + .../system/mapper/SysRoleMenuMapper.java | 13 + .../bashi/system/mapper/SysUserMapper.java | 46 + .../system/mapper/SysUserPostMapper.java | 13 + .../system/mapper/SysUserRoleMapper.java | 13 + .../system/service/ISysConfigService.java | 89 ++ .../bashi/system/service/ISysDeptService.java | 110 ++ .../system/service/ISysDictDataService.java | 67 ++ .../system/service/ISysDictTypeService.java | 105 ++ .../system/service/ISysLogininforService.java | 46 + .../bashi/system/service/ISysMenuService.java | 137 +++ .../system/service/ISysNoticeService.java | 66 + .../system/service/ISysOperLogService.java | 53 + .../bashi/system/service/ISysPostService.java | 106 ++ .../bashi/system/service/ISysRoleService.java | 137 +++ .../system/service/ISysUserOnlineService.java | 47 + .../bashi/system/service/ISysUserService.java | 178 +++ .../service/impl/SysConfigServiceImpl.java | 220 ++++ .../service/impl/SysDeptServiceImpl.java | 289 +++++ .../service/impl/SysDictDataServiceImpl.java | 124 ++ .../service/impl/SysDictTypeServiceImpl.java | 237 ++++ .../impl/SysLogininforServiceImpl.java | 94 ++ .../service/impl/SysMenuServiceImpl.java | 412 +++++++ .../service/impl/SysNoticeServiceImpl.java | 101 ++ .../service/impl/SysOperLogServiceImpl.java | 122 ++ .../service/impl/SysPostServiceImpl.java | 183 +++ .../service/impl/SysRoleServiceImpl.java | 308 +++++ .../impl/SysUserOnlineServiceImpl.java | 87 ++ .../service/impl/SysUserServiceImpl.java | 442 +++++++ .../mapper/system/SysConfigMapper.xml | 19 + .../resources/mapper/system/SysDeptMapper.xml | 73 ++ .../mapper/system/SysDictDataMapper.xml | 23 + .../mapper/system/SysDictTypeMapper.xml | 18 + .../mapper/system/SysLogininforMapper.xml | 19 + .../resources/mapper/system/SysMenuMapper.xml | 146 +++ .../mapper/system/SysNoticeMapper.xml | 20 + .../mapper/system/SysOperLogMapper.xml | 26 + .../resources/mapper/system/SysPostMapper.xml | 48 + .../mapper/system/SysRoleDeptMapper.xml | 12 + .../resources/mapper/system/SysRoleMapper.xml | 110 ++ .../mapper/system/SysRoleMenuMapper.xml | 12 + .../resources/mapper/system/SysUserMapper.xml | 165 +++ .../mapper/system/SysUserPostMapper.xml | 12 + .../mapper/system/SysUserRoleMapper.xml | 12 + hd_bg.png | Bin 0 -> 167874 bytes hd_bg1.png | Bin 0 -> 110000 bytes pom.xml | 292 +++++ sql/bus.sql | 351 ++++++ sql/dk.sql | 2 + sql/quartz.sql | 170 +++ sql/refresh.sql | 33 + sql/ry_20210210.sql | 688 +++++++++++ sql/test.sql | 171 +++ up.sql | 7 + 385 files changed, 31644 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 bashi-admin/pom.xml create mode 100644 bashi-admin/src/main/java/com/bashi/BaShiApplication.java create mode 100644 bashi-admin/src/main/java/com/bashi/BaShiServletInitializer.java create mode 100644 bashi-admin/src/main/java/com/bashi/com/UserDetailsServiceImpl.java create mode 100644 bashi-admin/src/main/java/com/bashi/web/controller/common/CaptchaController.java create mode 100644 bashi-admin/src/main/java/com/bashi/web/controller/common/CommonController.java create mode 100644 bashi-admin/src/main/java/com/bashi/web/controller/monitor/CacheController.java create mode 100644 bashi-admin/src/main/java/com/bashi/web/controller/monitor/SysLogininforController.java create mode 100644 bashi-admin/src/main/java/com/bashi/web/controller/monitor/SysOperlogController.java create mode 100644 bashi-admin/src/main/java/com/bashi/web/controller/monitor/SysUserOnlineController.java create mode 100644 bashi-admin/src/main/java/com/bashi/web/controller/system/SysConfigController.java create mode 100644 bashi-admin/src/main/java/com/bashi/web/controller/system/SysDeptController.java create mode 100644 bashi-admin/src/main/java/com/bashi/web/controller/system/SysDictDataController.java create mode 100644 bashi-admin/src/main/java/com/bashi/web/controller/system/SysDictTypeController.java create mode 100644 bashi-admin/src/main/java/com/bashi/web/controller/system/SysLoginController.java create mode 100644 bashi-admin/src/main/java/com/bashi/web/controller/system/SysMenuController.java create mode 100644 bashi-admin/src/main/java/com/bashi/web/controller/system/SysNoticeController.java create mode 100644 bashi-admin/src/main/java/com/bashi/web/controller/system/SysPostController.java create mode 100644 bashi-admin/src/main/java/com/bashi/web/controller/system/SysProfileController.java create mode 100644 bashi-admin/src/main/java/com/bashi/web/controller/system/SysRoleController.java create mode 100644 bashi-admin/src/main/java/com/bashi/web/controller/system/SysUserController.java create mode 100644 bashi-admin/src/main/resources/META-INF/spring-devtools.properties create mode 100644 bashi-admin/src/main/resources/application-dev.yml create mode 100644 bashi-admin/src/main/resources/application-prod.yml create mode 100644 bashi-admin/src/main/resources/application.yml create mode 100644 bashi-admin/src/main/resources/banner.txt create mode 100644 bashi-admin/src/main/resources/i18n/messages.properties create mode 100644 bashi-admin/src/main/resources/logback.xml create mode 100644 bashi-admin/src/test/java/com/bashi/BaseTest.java create mode 100644 bashi-common/pom.xml create mode 100644 bashi-common/src/main/java/com/bashi/common/annotation/DataScope.java create mode 100644 bashi-common/src/main/java/com/bashi/common/annotation/DataSource.java create mode 100644 bashi-common/src/main/java/com/bashi/common/annotation/Excel.java create mode 100644 bashi-common/src/main/java/com/bashi/common/annotation/Excels.java create mode 100644 bashi-common/src/main/java/com/bashi/common/annotation/Log.java create mode 100644 bashi-common/src/main/java/com/bashi/common/annotation/RepeatSubmit.java create mode 100644 bashi-common/src/main/java/com/bashi/common/com/Condition.java create mode 100644 bashi-common/src/main/java/com/bashi/common/com/PageParams.java create mode 100644 bashi-common/src/main/java/com/bashi/common/config/BsConfig.java create mode 100644 bashi-common/src/main/java/com/bashi/common/constant/Constants.java create mode 100644 bashi-common/src/main/java/com/bashi/common/constant/DateConstant.java create mode 100644 bashi-common/src/main/java/com/bashi/common/constant/GenConstants.java create mode 100644 bashi-common/src/main/java/com/bashi/common/constant/ScheduleConstants.java create mode 100644 bashi-common/src/main/java/com/bashi/common/constant/UserConstants.java create mode 100644 bashi-common/src/main/java/com/bashi/common/core/controller/BaseController.java create mode 100644 bashi-common/src/main/java/com/bashi/common/core/domain/AjaxResult.java create mode 100644 bashi-common/src/main/java/com/bashi/common/core/domain/BaseEntity.java create mode 100644 bashi-common/src/main/java/com/bashi/common/core/domain/TreeEntity.java create mode 100644 bashi-common/src/main/java/com/bashi/common/core/domain/TreeSelect.java create mode 100644 bashi-common/src/main/java/com/bashi/common/core/domain/entity/Customer.java create mode 100644 bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysDept.java create mode 100644 bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysDictData.java create mode 100644 bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysDictType.java create mode 100644 bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysMenu.java create mode 100644 bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysRole.java create mode 100644 bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysUser.java create mode 100644 bashi-common/src/main/java/com/bashi/common/core/domain/model/LoginBody.java create mode 100644 bashi-common/src/main/java/com/bashi/common/core/domain/model/LoginPhoneBody.java create mode 100644 bashi-common/src/main/java/com/bashi/common/core/domain/model/LoginUser.java create mode 100644 bashi-common/src/main/java/com/bashi/common/core/mybatisplus/cache/MybatisPlusRedisCache.java create mode 100644 bashi-common/src/main/java/com/bashi/common/core/mybatisplus/core/BaseMapperPlus.java create mode 100644 bashi-common/src/main/java/com/bashi/common/core/mybatisplus/methods/InsertAll.java create mode 100644 bashi-common/src/main/java/com/bashi/common/core/page/PagePlus.java create mode 100644 bashi-common/src/main/java/com/bashi/common/core/page/TableDataInfo.java create mode 100644 bashi-common/src/main/java/com/bashi/common/core/redis/RedisCache.java create mode 100644 bashi-common/src/main/java/com/bashi/common/enums/BusinessStatus.java create mode 100644 bashi-common/src/main/java/com/bashi/common/enums/BusinessType.java create mode 100644 bashi-common/src/main/java/com/bashi/common/enums/DataSourceType.java create mode 100644 bashi-common/src/main/java/com/bashi/common/enums/HttpMethod.java create mode 100644 bashi-common/src/main/java/com/bashi/common/enums/OperatorType.java create mode 100644 bashi-common/src/main/java/com/bashi/common/enums/UserStatus.java create mode 100644 bashi-common/src/main/java/com/bashi/common/exception/BaseException.java create mode 100644 bashi-common/src/main/java/com/bashi/common/exception/CustomException.java create mode 100644 bashi-common/src/main/java/com/bashi/common/exception/DemoModeException.java create mode 100644 bashi-common/src/main/java/com/bashi/common/exception/UtilException.java create mode 100644 bashi-common/src/main/java/com/bashi/common/exception/file/FileException.java create mode 100644 bashi-common/src/main/java/com/bashi/common/exception/file/FileNameLengthLimitExceededException.java create mode 100644 bashi-common/src/main/java/com/bashi/common/exception/file/FileSizeLimitExceededException.java create mode 100644 bashi-common/src/main/java/com/bashi/common/exception/file/InvalidExtensionException.java create mode 100644 bashi-common/src/main/java/com/bashi/common/exception/job/TaskException.java create mode 100644 bashi-common/src/main/java/com/bashi/common/exception/user/CaptchaException.java create mode 100644 bashi-common/src/main/java/com/bashi/common/exception/user/CaptchaExpireException.java create mode 100644 bashi-common/src/main/java/com/bashi/common/exception/user/UserException.java create mode 100644 bashi-common/src/main/java/com/bashi/common/exception/user/UserPasswordNotMatchException.java create mode 100644 bashi-common/src/main/java/com/bashi/common/filter/RepeatableFilter.java create mode 100644 bashi-common/src/main/java/com/bashi/common/filter/RepeatedlyRequestWrapper.java create mode 100644 bashi-common/src/main/java/com/bashi/common/filter/XssFilter.java create mode 100644 bashi-common/src/main/java/com/bashi/common/filter/XssHttpServletRequestWrapper.java create mode 100644 bashi-common/src/main/java/com/bashi/common/utils/BeanContextUtils.java create mode 100644 bashi-common/src/main/java/com/bashi/common/utils/BeanConvertUtil.java create mode 100644 bashi-common/src/main/java/com/bashi/common/utils/DateUtils.java create mode 100644 bashi-common/src/main/java/com/bashi/common/utils/DictUtils.java create mode 100644 bashi-common/src/main/java/com/bashi/common/utils/JsonUtils.java create mode 100644 bashi-common/src/main/java/com/bashi/common/utils/MessageUtils.java create mode 100644 bashi-common/src/main/java/com/bashi/common/utils/PageUtils.java create mode 100644 bashi-common/src/main/java/com/bashi/common/utils/SecurityUtils.java create mode 100644 bashi-common/src/main/java/com/bashi/common/utils/ServletUtils.java create mode 100644 bashi-common/src/main/java/com/bashi/common/utils/Threads.java create mode 100644 bashi-common/src/main/java/com/bashi/common/utils/file/FileTypeUtils.java create mode 100644 bashi-common/src/main/java/com/bashi/common/utils/file/FileUploadUtils.java create mode 100644 bashi-common/src/main/java/com/bashi/common/utils/file/FileUtils.java create mode 100644 bashi-common/src/main/java/com/bashi/common/utils/file/ImageUtils.java create mode 100644 bashi-common/src/main/java/com/bashi/common/utils/file/MimeTypeUtils.java create mode 100644 bashi-common/src/main/java/com/bashi/common/utils/ip/AddressUtils.java create mode 100644 bashi-common/src/main/java/com/bashi/common/utils/poi/ExcelUtil.java create mode 100644 bashi-common/src/main/java/com/bashi/common/utils/reflect/ReflectUtils.java create mode 100644 bashi-common/src/main/java/com/bashi/common/utils/spring/SpringUtils.java create mode 100644 bashi-common/src/main/java/com/bashi/common/utils/sql/SqlUtil.java create mode 100644 bashi-dk/pom.xml create mode 100644 bashi-dk/src/main/java/com/bashi/dk/controller/BorrowStatusController.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/controller/DkAgreementSettingController.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/controller/DkBorrowController.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/controller/DkCustomerController.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/controller/DkCustomerInfoController.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/controller/DkHomeSettingController.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/controller/DkLoansSettingController.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/controller/app/AppBorrowController.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/controller/app/AppCustomerController.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/controller/app/AppCustomerOpenController.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/controller/app/AppHomeController.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/controller/app/AppSettingController.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/controller/app/LoginV2Controller.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/controller/app/V2CommonController.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/domain/AgreementSetting.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/domain/Borrow.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/domain/BorrowLog.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/domain/BorrowStatus.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/domain/CustomerInfo.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/domain/HomeSetting.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/domain/LoansSetting.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/dto/admin/req/BorrowUpdateStatusReq.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/dto/admin/req/UpdatePwdCustomerReq.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/dto/admin/resp/BorrowResp.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/dto/admin/resp/CustomerAdminResp.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/dto/admin/resp/CustomerExportVo.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/dto/admin/resp/CustomerInfo.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/dto/app/req/BorrowStartReq.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/dto/app/req/CalLoanReq.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/dto/app/req/CustomerRegisterReq.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/dto/app/req/UpdatePwdOpenReq.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/BorrowInfo.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/BorrowStepResp.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/CalLoanResp.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/LoanProcessResp.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/LoanUser.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/LoansSettingVO.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/enums/BankTypeEnums.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/kit/CalLoanManager.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/kit/DkLoginKit.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/manager/FileUploadManager.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/manager/FileUploadRes.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/mapper/AgreementSettingMapper.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/mapper/BorrowLogMapper.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/mapper/BorrowMapper.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/mapper/BorrowStatusMapper.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/mapper/CustomerInfoMapper.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/mapper/CustomerMapper.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/mapper/HomeSettingMapper.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/mapper/LoansSettingMapper.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/oss/ali/AliOssKit.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/oss/ali/CosConfig.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/oss/ali/CosKit.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/oss/ali/OssConfig.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/service/AgreementSettingService.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/service/BorrowService.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/service/BorrowStatusService.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/service/CustomerInfoService.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/service/CustomerService.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/service/HomeSettingService.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/service/LoansSettingService.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/service/impl/AgreementSettingServiceImpl.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/service/impl/BorrowServiceImpl.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/service/impl/BorrowStatusServiceImpl.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/service/impl/CustomerInfoServiceImpl.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/service/impl/CustomerServiceImpl.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/service/impl/HomeSettingServiceImpl.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/service/impl/LoansSettingServiceImpl.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/util/ACMLoanCalculator.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/util/ACPIMLoanCalculator.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/util/ContentReplaceUtil.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/util/ILoanCalculator.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/util/ImageUtil.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/util/Loan.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/util/LoanByMonth.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/util/LoanCalculatorAdapter.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/util/LoanCalculatorTest.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/util/LoanUtil.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/util/MoneyUtil.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/util/OrderTradeNoUtil.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/util/PhoneRandomUtil.java create mode 100644 bashi-dk/src/main/java/com/bashi/dk/util/SnowFlake.java create mode 100644 bashi-dk/src/main/resources/mapper/BorrowMapper.xml create mode 100644 bashi-dk/src/main/resources/mapper/CustomerMapper.xml create mode 100644 bashi-framework/pom.xml create mode 100644 bashi-framework/src/main/java/com/bashi/framework/app/AppTypeEnums.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/app/AppTypeFactory.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/aspectj/DataScopeAspect.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/aspectj/DataSourceAspect.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/aspectj/LogAspect.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/captcha/UnsignedMathGenerator.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/AdminServerConfig.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/ApplicationConfig.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/AsyncConfig.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/CaptchaConfig.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/DruidConfig.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/FeignConfig.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/FilterConfig.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/JacksonConfig.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/MybatisPlusConfig.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/RedisConfig.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/ResourcesConfig.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/SecurityConfig.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/ServerConfig.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/SmsCodeAuthenticationSecurityConfig.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/SwaggerConfig.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/ThreadPoolConfig.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/ValidatorConfig.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/WebSocketConfig.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/WebSocketServer.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/WebSocketServerDemo.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/properties/CaptchaProperties.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/properties/RedissonProperties.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/properties/SwaggerProperties.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/properties/ThreadPoolProperties.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/properties/TokenProperties.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/config/properties/XssProperties.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/constant/CodeType.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/dto/WebSocketMessageDTO.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/dto/WebSocketMessageType.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/interceptor/RepeatSubmitInterceptor.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/interceptor/impl/SameUrlDataInterceptor.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/manager/ShutdownManager.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/mybatisplus/CreateAndUpdateMetaObjectHandler.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/security/filter/JwtAuthenticationTokenFilter.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/security/handle/AuthenticationEntryPointImpl.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/security/handle/LogoutSuccessHandlerImpl.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/security/sms/IDuteUserDetailsService.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/security/sms/LoginTypeEnums.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/security/sms/SmsAuthenticationFailureHandler.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/security/sms/SmsAuthenticationFilter.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/security/sms/SmsAuthenticationProvider.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/security/sms/SmsAuthenticationSuccessHandler.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/security/sms/SmsAuthenticationToken.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/util/AgentUtils.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/web/exception/GlobalExceptionHandler.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/web/service/AsyncService.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/web/service/CodeService.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/web/service/CustomerService.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/web/service/PermissionService.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/web/service/RegisterEventService.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/web/service/SysLoginService.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/web/service/SysPermissionService.java create mode 100644 bashi-framework/src/main/java/com/bashi/framework/web/service/TokenService.java create mode 100644 bashi-generator/pom.xml create mode 100644 bashi-generator/src/main/java/com/bashi/generator/config/GenConfig.java create mode 100644 bashi-generator/src/main/java/com/bashi/generator/controller/GenController.java create mode 100644 bashi-generator/src/main/java/com/bashi/generator/domain/GenTable.java create mode 100644 bashi-generator/src/main/java/com/bashi/generator/domain/GenTableColumn.java create mode 100644 bashi-generator/src/main/java/com/bashi/generator/mapper/GenTableColumnMapper.java create mode 100644 bashi-generator/src/main/java/com/bashi/generator/mapper/GenTableMapper.java create mode 100644 bashi-generator/src/main/java/com/bashi/generator/service/GenTableColumnServiceImpl.java create mode 100644 bashi-generator/src/main/java/com/bashi/generator/service/GenTableServiceImpl.java create mode 100644 bashi-generator/src/main/java/com/bashi/generator/service/IGenTableColumnService.java create mode 100644 bashi-generator/src/main/java/com/bashi/generator/service/IGenTableService.java create mode 100644 bashi-generator/src/main/java/com/bashi/generator/util/GenUtils.java create mode 100644 bashi-generator/src/main/java/com/bashi/generator/util/VelocityInitializer.java create mode 100644 bashi-generator/src/main/java/com/bashi/generator/util/VelocityUtils.java create mode 100644 bashi-generator/src/main/resources/generator.yml create mode 100644 bashi-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml create mode 100644 bashi-generator/src/main/resources/mapper/generator/GenTableMapper.xml create mode 100644 bashi-generator/src/main/resources/vm/java/addBo.java.vm create mode 100644 bashi-generator/src/main/resources/vm/java/controller.java.vm create mode 100644 bashi-generator/src/main/resources/vm/java/domain.java.vm create mode 100644 bashi-generator/src/main/resources/vm/java/editBo.java.vm create mode 100644 bashi-generator/src/main/resources/vm/java/mapper.java.vm create mode 100644 bashi-generator/src/main/resources/vm/java/queryBo.java.vm create mode 100644 bashi-generator/src/main/resources/vm/java/service.java.vm create mode 100644 bashi-generator/src/main/resources/vm/java/serviceImpl.java.vm create mode 100644 bashi-generator/src/main/resources/vm/java/sub-domain.java.vm create mode 100644 bashi-generator/src/main/resources/vm/java/vo.java.vm create mode 100644 bashi-generator/src/main/resources/vm/js/api.js.vm create mode 100644 bashi-generator/src/main/resources/vm/sql/sql.vm create mode 100644 bashi-generator/src/main/resources/vm/vue/index-tree.vue.vm create mode 100644 bashi-generator/src/main/resources/vm/vue/index.vue.vm create mode 100644 bashi-generator/src/main/resources/vm/xml/mapper.xml.vm create mode 100644 bashi-quartz/pom.xml create mode 100644 bashi-quartz/src/main/java/com/bashi/quartz/config/ScheduleConfig.java create mode 100644 bashi-quartz/src/main/java/com/bashi/quartz/controller/SysJobController.java create mode 100644 bashi-quartz/src/main/java/com/bashi/quartz/controller/SysJobLogController.java create mode 100644 bashi-quartz/src/main/java/com/bashi/quartz/domain/SysJob.java create mode 100644 bashi-quartz/src/main/java/com/bashi/quartz/domain/SysJobLog.java create mode 100644 bashi-quartz/src/main/java/com/bashi/quartz/mapper/SysJobLogMapper.java create mode 100644 bashi-quartz/src/main/java/com/bashi/quartz/mapper/SysJobMapper.java create mode 100644 bashi-quartz/src/main/java/com/bashi/quartz/service/ISysJobLogService.java create mode 100644 bashi-quartz/src/main/java/com/bashi/quartz/service/ISysJobService.java create mode 100644 bashi-quartz/src/main/java/com/bashi/quartz/service/impl/SysJobLogServiceImpl.java create mode 100644 bashi-quartz/src/main/java/com/bashi/quartz/service/impl/SysJobServiceImpl.java create mode 100644 bashi-quartz/src/main/java/com/bashi/quartz/task/RyTask.java create mode 100644 bashi-quartz/src/main/java/com/bashi/quartz/util/AbstractQuartzJob.java create mode 100644 bashi-quartz/src/main/java/com/bashi/quartz/util/CronUtils.java create mode 100644 bashi-quartz/src/main/java/com/bashi/quartz/util/JobInvokeUtil.java create mode 100644 bashi-quartz/src/main/java/com/bashi/quartz/util/QuartzDisallowConcurrentExecution.java create mode 100644 bashi-quartz/src/main/java/com/bashi/quartz/util/QuartzJobExecution.java create mode 100644 bashi-quartz/src/main/java/com/bashi/quartz/util/ScheduleUtils.java create mode 100644 bashi-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml create mode 100644 bashi-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml create mode 100644 bashi-system/pom.xml create mode 100644 bashi-system/src/main/java/com/bashi/system/domain/SysConfig.java create mode 100644 bashi-system/src/main/java/com/bashi/system/domain/SysLogininfor.java create mode 100644 bashi-system/src/main/java/com/bashi/system/domain/SysNotice.java create mode 100644 bashi-system/src/main/java/com/bashi/system/domain/SysOperLog.java create mode 100644 bashi-system/src/main/java/com/bashi/system/domain/SysPost.java create mode 100644 bashi-system/src/main/java/com/bashi/system/domain/SysRoleDept.java create mode 100644 bashi-system/src/main/java/com/bashi/system/domain/SysRoleMenu.java create mode 100644 bashi-system/src/main/java/com/bashi/system/domain/SysUserOnline.java create mode 100644 bashi-system/src/main/java/com/bashi/system/domain/SysUserPost.java create mode 100644 bashi-system/src/main/java/com/bashi/system/domain/SysUserRole.java create mode 100644 bashi-system/src/main/java/com/bashi/system/domain/vo/MetaVo.java create mode 100644 bashi-system/src/main/java/com/bashi/system/domain/vo/RouterVo.java create mode 100644 bashi-system/src/main/java/com/bashi/system/mapper/SysConfigMapper.java create mode 100644 bashi-system/src/main/java/com/bashi/system/mapper/SysDeptMapper.java create mode 100644 bashi-system/src/main/java/com/bashi/system/mapper/SysDictDataMapper.java create mode 100644 bashi-system/src/main/java/com/bashi/system/mapper/SysDictTypeMapper.java create mode 100644 bashi-system/src/main/java/com/bashi/system/mapper/SysLogininforMapper.java create mode 100644 bashi-system/src/main/java/com/bashi/system/mapper/SysMenuMapper.java create mode 100644 bashi-system/src/main/java/com/bashi/system/mapper/SysNoticeMapper.java create mode 100644 bashi-system/src/main/java/com/bashi/system/mapper/SysOperLogMapper.java create mode 100644 bashi-system/src/main/java/com/bashi/system/mapper/SysPostMapper.java create mode 100644 bashi-system/src/main/java/com/bashi/system/mapper/SysRoleDeptMapper.java create mode 100644 bashi-system/src/main/java/com/bashi/system/mapper/SysRoleMapper.java create mode 100644 bashi-system/src/main/java/com/bashi/system/mapper/SysRoleMenuMapper.java create mode 100644 bashi-system/src/main/java/com/bashi/system/mapper/SysUserMapper.java create mode 100644 bashi-system/src/main/java/com/bashi/system/mapper/SysUserPostMapper.java create mode 100644 bashi-system/src/main/java/com/bashi/system/mapper/SysUserRoleMapper.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/ISysConfigService.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/ISysDeptService.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/ISysDictDataService.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/ISysDictTypeService.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/ISysLogininforService.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/ISysMenuService.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/ISysNoticeService.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/ISysOperLogService.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/ISysPostService.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/ISysRoleService.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/ISysUserOnlineService.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/ISysUserService.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/impl/SysConfigServiceImpl.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/impl/SysDeptServiceImpl.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/impl/SysDictDataServiceImpl.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/impl/SysDictTypeServiceImpl.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/impl/SysLogininforServiceImpl.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/impl/SysMenuServiceImpl.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/impl/SysNoticeServiceImpl.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/impl/SysOperLogServiceImpl.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/impl/SysPostServiceImpl.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/impl/SysRoleServiceImpl.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/impl/SysUserOnlineServiceImpl.java create mode 100644 bashi-system/src/main/java/com/bashi/system/service/impl/SysUserServiceImpl.java create mode 100644 bashi-system/src/main/resources/mapper/system/SysConfigMapper.xml create mode 100644 bashi-system/src/main/resources/mapper/system/SysDeptMapper.xml create mode 100644 bashi-system/src/main/resources/mapper/system/SysDictDataMapper.xml create mode 100644 bashi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml create mode 100644 bashi-system/src/main/resources/mapper/system/SysLogininforMapper.xml create mode 100644 bashi-system/src/main/resources/mapper/system/SysMenuMapper.xml create mode 100644 bashi-system/src/main/resources/mapper/system/SysNoticeMapper.xml create mode 100644 bashi-system/src/main/resources/mapper/system/SysOperLogMapper.xml create mode 100644 bashi-system/src/main/resources/mapper/system/SysPostMapper.xml create mode 100644 bashi-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml create mode 100644 bashi-system/src/main/resources/mapper/system/SysRoleMapper.xml create mode 100644 bashi-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml create mode 100644 bashi-system/src/main/resources/mapper/system/SysUserMapper.xml create mode 100644 bashi-system/src/main/resources/mapper/system/SysUserPostMapper.xml create mode 100644 bashi-system/src/main/resources/mapper/system/SysUserRoleMapper.xml create mode 100644 hd_bg.png create mode 100644 hd_bg1.png create mode 100644 pom.xml create mode 100644 sql/bus.sql create mode 100644 sql/dk.sql create mode 100644 sql/quartz.sql create mode 100644 sql/refresh.sql create mode 100644 sql/ry_20210210.sql create mode 100644 sql/test.sql create mode 100644 up.sql diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8cfd370 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# http://editorconfig.org +root = true + +# 空格替代Tab缩进在各种编辑工具下效果一致 +[*] +indent_style = space +indent_size = 4 +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true + +[*.java] +indent_style = tab + +[*.{json,yml}] +indent_size = 2 + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c0e049 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +###################################################################### +# Build Tools + +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar + +target/ +!.mvn/wrapper/maven-wrapper.jar + +###################################################################### +# IDE + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +nbproject/private/ +build/* +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +###################################################################### +# Others +*.log +*.xml.versionsBackup +*.swp + +!*/build/*.java +!*/build/*.html +!*/build/*.xml diff --git a/bashi-admin/pom.xml b/bashi-admin/pom.xml new file mode 100644 index 0000000..31a5dca --- /dev/null +++ b/bashi-admin/pom.xml @@ -0,0 +1,105 @@ + + + + bashi + com.bashi + 2.4.0 + + 4.0.0 + jar + bashi-admin + + + web服务入口 + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + junit + junit + test + + + + + org.springframework.boot + spring-boot-devtools + true + + + + + mysql + mysql-connector-java + + + + com.bashi + bashi-dk + 2.4.0 + + + + + + + + com.bashi + bashi-generator + + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + true + + + + + repackage + + + + + + org.apache.maven.plugins + maven-war-plugin + 3.1.0 + + false + ${project.artifactId} + + + + org.apache.maven.plugins + 2.6 + maven-resources-plugin + + UTF-8 + + xlsx + p12 + + + + + + + diff --git a/bashi-admin/src/main/java/com/bashi/BaShiApplication.java b/bashi-admin/src/main/java/com/bashi/BaShiApplication.java new file mode 100644 index 0000000..e9dd046 --- /dev/null +++ b/bashi-admin/src/main/java/com/bashi/BaShiApplication.java @@ -0,0 +1,24 @@ +package com.bashi; + +import org.dromara.x.file.storage.spring.EnableFileStorage; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * 启动程序 + * + * @author duteliang + */ + +@SpringBootApplication +@EnableScheduling +@EnableFileStorage +public class BaShiApplication { + public static void main(String[] args) { + System.setProperty("spring.devtools.restart.enabled", "false"); + System.setProperty("druid.mysql.usePingMethod", "false"); + SpringApplication.run(BaShiApplication.class, args); + System.out.println("(♥◠‿◠)ノ゙ 巴适启动成功 ლ(´ڡ`ლ)゙"); + } +} diff --git a/bashi-admin/src/main/java/com/bashi/BaShiServletInitializer.java b/bashi-admin/src/main/java/com/bashi/BaShiServletInitializer.java new file mode 100644 index 0000000..5e3f3b4 --- /dev/null +++ b/bashi-admin/src/main/java/com/bashi/BaShiServletInitializer.java @@ -0,0 +1,18 @@ +package com.bashi; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +/** + * web容器中进行部署 + * + * @author duteliang + */ +public class BaShiServletInitializer extends SpringBootServletInitializer +{ + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) + { + return application.sources(BaShiApplication.class); + } +} diff --git a/bashi-admin/src/main/java/com/bashi/com/UserDetailsServiceImpl.java b/bashi-admin/src/main/java/com/bashi/com/UserDetailsServiceImpl.java new file mode 100644 index 0000000..18a171e --- /dev/null +++ b/bashi-admin/src/main/java/com/bashi/com/UserDetailsServiceImpl.java @@ -0,0 +1,105 @@ +package com.bashi.com; + +import cn.hutool.core.lang.Validator; +import com.bashi.common.core.domain.entity.Customer; +import com.bashi.common.core.domain.entity.SysUser; +import com.bashi.common.core.domain.model.LoginPhoneBody; +import com.bashi.common.core.domain.model.LoginUser; +import com.bashi.common.enums.UserStatus; +import com.bashi.common.exception.BaseException; +import com.bashi.common.exception.CustomException; +import com.bashi.dk.service.CustomerService; +import com.bashi.framework.security.sms.IDuteUserDetailsService; +import com.bashi.framework.security.sms.LoginTypeEnums; +import com.bashi.framework.web.service.RegisterEventService; +import com.bashi.framework.web.service.SysPermissionService; +import com.bashi.system.service.ISysUserService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; + +/** + * 用户验证处理 + * + * @author duteliang + */ +@Service +public class UserDetailsServiceImpl implements IDuteUserDetailsService { + private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class); + + @Autowired + private ISysUserService userService; + @Autowired + private SysPermissionService permissionService; + @Autowired(required = false) + private Map list; + @Autowired + private CustomerService customerService; + @Autowired + private BCryptPasswordEncoder passwordEncoder; + @PostConstruct + public void init(){ + System.out.println("daaaa"); + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + SysUser user = userService.selectUserByUserName(username); + if (Validator.isNull(user)) { + log.info("登录用户:{} 不存在.", username); + throw new UsernameNotFoundException("登录用户:" + username + " 不存在"); + } else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) { + log.info("登录用户:{} 已被删除.", username); + throw new BaseException("对不起,您的账号:" + username + " 已被删除"); + } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) { + log.info("登录用户:{} 已被停用.", username); + throw new BaseException("对不起,您的账号:" + username + " 已停用"); + } + return createLoginUser(user); + } + + public UserDetails createLoginUser(SysUser user) { + return new LoginUser(user, permissionService.getMenuPermission(user)); + } + + public UserDetails createCustomerUser(Customer customer) { + LoginUser loginUser = new LoginUser(null, new HashSet<>()); + loginUser.setCustomer(customer); + loginUser.setType(1); + return loginUser; + } + + @Override + public UserDetails loadUserByMobile(LoginPhoneBody body) throws UsernameNotFoundException { + if(body == null){ + throw new CustomException("账号不存在,登陆失败"); + } + String mobile = body.getMobile(); + Customer customer = null; + if(Objects.equals(body.getLoginRole(), LoginTypeEnums.CUSTOMER_PASSWORD.getCode())){ + customer = customerService.getCustomerByName(mobile); + if(customer == null || !passwordEncoder.matches(body.getPassword(),customer.getPassword())){ + log.info("登录用户:{} 用户名不存在或者密码错误", mobile); + throw new BaseException("对不起,用户名不存在或者密码错误"); + } + } + if (customer == null){ + log.info("登录用户:{} 不存在.", mobile); + throw new UsernameNotFoundException("登录用户:" + mobile + " 不存在"); + } else if (customer.getStatus() == 1) { + log.info("登录用户:{} 已被停用.", mobile); + throw new BaseException("对不起,您的账号:" + mobile + " 已停用"); + } + return createCustomerUser(customer); + } + +} diff --git a/bashi-admin/src/main/java/com/bashi/web/controller/common/CaptchaController.java b/bashi-admin/src/main/java/com/bashi/web/controller/common/CaptchaController.java new file mode 100644 index 0000000..5920b9e --- /dev/null +++ b/bashi-admin/src/main/java/com/bashi/web/controller/common/CaptchaController.java @@ -0,0 +1,121 @@ +package com.bashi.web.controller.common; + +import cn.hutool.captcha.AbstractCaptcha; +import cn.hutool.captcha.CircleCaptcha; +import cn.hutool.captcha.LineCaptcha; +import cn.hutool.captcha.ShearCaptcha; +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import com.bashi.common.constant.Constants; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.redis.RedisCache; +import com.bashi.framework.captcha.UnsignedMathGenerator; +import com.bashi.framework.config.properties.CaptchaProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * 验证码操作处理 + * + * @author Lion Li + */ +@RestController +public class CaptchaController { + + // 圆圈干扰验证码 + @Resource(name = "CircleCaptcha") + private CircleCaptcha circleCaptcha; + // 线段干扰的验证码 + @Resource(name = "LineCaptcha") + private LineCaptcha lineCaptcha; + // 扭曲干扰验证码 + @Resource(name = "ShearCaptcha") + private ShearCaptcha shearCaptcha; + + @Autowired + private RedisCache redisCache; + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 生成验证码 + */ + @GetMapping("/captchaImage") + public AjaxResult getCode() { + Map ajax = new HashMap<>(); + Boolean enabled = captchaProperties.getEnabled(); + ajax.put("enabled", enabled); + if (!enabled) { + return AjaxResult.success(ajax); + } + // 保存验证码信息 + String uuid = IdUtil.simpleUUID(); + String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid; + String code = null; + // 生成验证码 + CodeGenerator codeGenerator; + AbstractCaptcha captcha; + switch (captchaProperties.getType()) { + case "math": + codeGenerator = new UnsignedMathGenerator(captchaProperties.getNumberLength()); + break; + case "char": + codeGenerator = new RandomGenerator(captchaProperties.getCharLength()); + break; + default: + throw new IllegalArgumentException("验证码类型异常"); + } + switch (captchaProperties.getCategory()) { + case "line": + captcha = lineCaptcha; + break; + case "circle": + captcha = circleCaptcha; + break; + case "shear": + captcha = shearCaptcha; + break; + default: + throw new IllegalArgumentException("验证码类别异常"); + } + captcha.setGenerator(codeGenerator); + captcha.createCode(); + if ("math".equals(captchaProperties.getType())) { + code = getCodeResult(captcha.getCode()); + } else if ("char".equals(captchaProperties.getType())) { + code = captcha.getCode(); + } + redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES); + ajax.put("uuid", uuid); + ajax.put("img", captcha.getImageBase64()); + return AjaxResult.success(ajax); + } + + private String getCodeResult(String capStr) { + int numberLength = captchaProperties.getNumberLength(); + int a = Convert.toInt(StrUtil.sub(capStr, 0, numberLength).trim()); + char operator = capStr.charAt(numberLength); + int b = Convert.toInt(StrUtil.sub(capStr, numberLength + 1, numberLength + 1 + numberLength).trim()); + switch (operator) { + case '*': + return a * b + ""; + case '+': + return a + b + ""; + case '-': + return a - b + ""; + default: + return ""; + } + } + +} diff --git a/bashi-admin/src/main/java/com/bashi/web/controller/common/CommonController.java b/bashi-admin/src/main/java/com/bashi/web/controller/common/CommonController.java new file mode 100644 index 0000000..2ccaaf8 --- /dev/null +++ b/bashi-admin/src/main/java/com/bashi/web/controller/common/CommonController.java @@ -0,0 +1,107 @@ +package com.bashi.web.controller.common; + +import cn.hutool.core.util.StrUtil; +import com.bashi.common.config.BsConfig; +import com.bashi.common.constant.Constants; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.utils.file.FileUploadUtils; +import com.bashi.common.utils.file.FileUtils; +import com.bashi.framework.config.ServerConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +/** + * 通用请求处理 + * + * @author duteliang + */ +@RestController +public class CommonController { + private static final Logger log = LoggerFactory.getLogger(CommonController.class); + + @Autowired + private ServerConfig serverConfig; + + /** + * 通用下载请求 + * + * @param fileName 文件名称 + * @param delete 是否删除 + */ + @GetMapping("common/download") + public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request) { + try { + if (!FileUtils.checkAllowDownload(fileName)) { + throw new Exception(StrUtil.format("文件名称({})非法,不允许下载。 ", fileName)); + } + String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1); + String filePath = BsConfig.getDownloadPath() + fileName; + File file = new File(filePath); + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + FileUtils.setAttachmentResponseHeader(response, realFileName); + FileUtils.writeToStream(file, response.getOutputStream()); + if (delete) { + FileUtils.del(file); + } + } catch (Exception e) { + log.error("下载文件失败", e); + } + } + + /** + * 通用上传请求 + */ + @PostMapping("/common/upload") + public AjaxResult uploadFile(MultipartFile file) throws Exception { + try { + // 上传文件路径 + String filePath = BsConfig.getUploadPath(); + // 上传并返回新文件名称 + String fileName = FileUploadUtils.upload(filePath, file); + String url = serverConfig.getUrl() + fileName; + Map ajax = new HashMap<>(); + ajax.put("fileName", fileName); + ajax.put("url", url); + return AjaxResult.success(ajax); + } catch (Exception e) { + return AjaxResult.error(e.getMessage()); + } + } + + /** + * 本地资源通用下载 + */ + @GetMapping("/common/download/resource") + public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response) + throws Exception { + try { + if (!FileUtils.checkAllowDownload(resource)) { + throw new Exception(StrUtil.format("资源文件({})非法,不允许下载。 ", resource)); + } + // 本地资源路径 + String localPath = BsConfig.getProfile(); + // 数据库资源地址 + String downloadPath = localPath + StrUtil.subAfter(resource, Constants.RESOURCE_PREFIX, false); + // 下载名称 + String downloadName = StrUtil.subAfter(downloadPath, "/", true); + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + File file = new File(downloadPath); + FileUtils.setAttachmentResponseHeader(response, downloadName); + FileUtils.writeToStream(file, response.getOutputStream()); + } catch (Exception e) { + log.error("下载文件失败", e); + } + } +} diff --git a/bashi-admin/src/main/java/com/bashi/web/controller/monitor/CacheController.java b/bashi-admin/src/main/java/com/bashi/web/controller/monitor/CacheController.java new file mode 100644 index 0000000..f21a502 --- /dev/null +++ b/bashi-admin/src/main/java/com/bashi/web/controller/monitor/CacheController.java @@ -0,0 +1,50 @@ +package com.bashi.web.controller.monitor; + +import cn.hutool.core.util.StrUtil; +import com.bashi.common.core.domain.AjaxResult; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.*; + +/** + * 缓存监控 + * + * @author duteliang + */ +@RestController +@RequestMapping("/monitor/cache") +public class CacheController +{ + @Autowired + private RedisTemplate redisTemplate; + + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @GetMapping() + public AjaxResult getInfo() throws Exception + { + Properties info = (Properties) redisTemplate.execute((RedisCallback) connection -> connection.info()); + Properties commandStats = (Properties) redisTemplate.execute((RedisCallback) connection -> connection.info("commandstats")); + Object dbSize = redisTemplate.execute((RedisCallback) connection -> connection.dbSize()); + + Map result = new HashMap<>(3); + result.put("info", info); + result.put("dbSize", dbSize); + + List> pieList = new ArrayList<>(); + commandStats.stringPropertyNames().forEach(key -> { + Map data = new HashMap<>(2); + String property = commandStats.getProperty(key); + data.put("name", StrUtil.removePrefix(key, "cmdstat_")); + data.put("value", StrUtil.subBetween(property, "calls=", ",usec")); + pieList.add(data); + }); + result.put("commandStats", pieList); + return AjaxResult.success(result); + } +} diff --git a/bashi-admin/src/main/java/com/bashi/web/controller/monitor/SysLogininforController.java b/bashi-admin/src/main/java/com/bashi/web/controller/monitor/SysLogininforController.java new file mode 100644 index 0000000..bc51e18 --- /dev/null +++ b/bashi-admin/src/main/java/com/bashi/web/controller/monitor/SysLogininforController.java @@ -0,0 +1,62 @@ +package com.bashi.web.controller.monitor; + +import com.bashi.common.annotation.Log; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.enums.BusinessType; +import com.bashi.common.utils.poi.ExcelUtil; +import com.bashi.system.domain.SysLogininfor; +import com.bashi.system.service.ISysLogininforService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 系统访问记录 + * + * @author duteliang + */ +@RestController +@RequestMapping("/monitor/logininfor") +public class SysLogininforController extends BaseController +{ + @Autowired + private ISysLogininforService logininforService; + + @PreAuthorize("@ss.hasPermi('monitor:logininfor:list')") + @GetMapping("/list") + public TableDataInfo list(SysLogininfor logininfor) + { + return logininforService.selectPageLogininforList(logininfor); + } + + @Log(title = "登录日志", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('monitor:logininfor:export')") + @GetMapping("/export") + public AjaxResult export(SysLogininfor logininfor) + { + List list = logininforService.selectLogininforList(logininfor); + ExcelUtil util = new ExcelUtil(SysLogininfor.class); + return util.exportExcel(list, "登录日志"); + } + + @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')") + @Log(title = "登录日志", businessType = BusinessType.DELETE) + @DeleteMapping("/{infoIds}") + public AjaxResult remove(@PathVariable Long[] infoIds) + { + return toAjax(logininforService.deleteLogininforByIds(infoIds)); + } + + @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')") + @Log(title = "登录日志", businessType = BusinessType.CLEAN) + @DeleteMapping("/clean") + public AjaxResult clean() + { + logininforService.cleanLogininfor(); + return AjaxResult.success(); + } +} diff --git a/bashi-admin/src/main/java/com/bashi/web/controller/monitor/SysOperlogController.java b/bashi-admin/src/main/java/com/bashi/web/controller/monitor/SysOperlogController.java new file mode 100644 index 0000000..262dad5 --- /dev/null +++ b/bashi-admin/src/main/java/com/bashi/web/controller/monitor/SysOperlogController.java @@ -0,0 +1,62 @@ +package com.bashi.web.controller.monitor; + +import com.bashi.common.annotation.Log; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.enums.BusinessType; +import com.bashi.common.utils.poi.ExcelUtil; +import com.bashi.system.domain.SysOperLog; +import com.bashi.system.service.ISysOperLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 操作日志记录 + * + * @author duteliang + */ +@RestController +@RequestMapping("/monitor/operlog") +public class SysOperlogController extends BaseController +{ + @Autowired + private ISysOperLogService operLogService; + + @PreAuthorize("@ss.hasPermi('monitor:operlog:list')") + @GetMapping("/list") + public TableDataInfo list(SysOperLog operLog) + { + return operLogService.selectPageOperLogList(operLog); + } + + @Log(title = "操作日志", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('monitor:operlog:export')") + @GetMapping("/export") + public AjaxResult export(SysOperLog operLog) + { + List list = operLogService.selectOperLogList(operLog); + ExcelUtil util = new ExcelUtil(SysOperLog.class); + return util.exportExcel(list, "操作日志"); + } + + @Log(title = "操作日志", businessType = BusinessType.DELETE) + @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')") + @DeleteMapping("/{operIds}") + public AjaxResult remove(@PathVariable Long[] operIds) + { + return toAjax(operLogService.deleteOperLogByIds(operIds)); + } + + @Log(title = "操作日志", businessType = BusinessType.CLEAN) + @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')") + @DeleteMapping("/clean") + public AjaxResult clean() + { + operLogService.cleanOperLog(); + return AjaxResult.success(); + } +} diff --git a/bashi-admin/src/main/java/com/bashi/web/controller/monitor/SysUserOnlineController.java b/bashi-admin/src/main/java/com/bashi/web/controller/monitor/SysUserOnlineController.java new file mode 100644 index 0000000..02ff0aa --- /dev/null +++ b/bashi-admin/src/main/java/com/bashi/web/controller/monitor/SysUserOnlineController.java @@ -0,0 +1,91 @@ +package com.bashi.web.controller.monitor; + +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.StrUtil; +import com.bashi.common.annotation.Log; +import com.bashi.common.constant.Constants; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.domain.model.LoginUser; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.core.redis.RedisCache; +import com.bashi.common.enums.BusinessType; +import com.bashi.common.utils.PageUtils; +import com.bashi.system.domain.SysUserOnline; +import com.bashi.system.service.ISysUserOnlineService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * 在线用户监控 + * + * @author duteliang + */ +@RestController +@RequestMapping("/monitor/online") +public class SysUserOnlineController extends BaseController +{ + @Autowired + private ISysUserOnlineService userOnlineService; + + @Autowired + private RedisCache redisCache; + + @PreAuthorize("@ss.hasPermi('monitor:online:list')") + @GetMapping("/list") + public TableDataInfo list(String ipaddr, String userName) + { + Collection keys = redisCache.keys(Constants.LOGIN_TOKEN_KEY + "*"); + List userOnlineList = new ArrayList(); + for (String key : keys) + { + LoginUser user = redisCache.getCacheObject(key); + if (Validator.isNotEmpty(ipaddr) && Validator.isNotEmpty(userName)) + { + if (StrUtil.equals(ipaddr, user.getIpaddr()) && StrUtil.equals(userName, user.getUsername())) + { + userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user)); + } + } + else if (Validator.isNotEmpty(ipaddr)) + { + if (StrUtil.equals(ipaddr, user.getIpaddr())) + { + userOnlineList.add(userOnlineService.selectOnlineByIpaddr(ipaddr, user)); + } + } + else if (Validator.isNotEmpty(userName) && Validator.isNotNull(user.getUser())) + { + if (StrUtil.equals(userName, user.getUsername())) + { + userOnlineList.add(userOnlineService.selectOnlineByUserName(userName, user)); + } + } + else + { + userOnlineList.add(userOnlineService.loginUserToUserOnline(user)); + } + } + Collections.reverse(userOnlineList); + userOnlineList.removeAll(Collections.singleton(null)); + return PageUtils.buildDataInfo(userOnlineList); + } + + /** + * 强退用户 + */ + @PreAuthorize("@ss.hasPermi('monitor:online:forceLogout')") + @Log(title = "在线用户", businessType = BusinessType.FORCE) + @DeleteMapping("/{tokenId}") + public AjaxResult forceLogout(@PathVariable String tokenId) + { + redisCache.deleteObject(Constants.LOGIN_TOKEN_KEY + tokenId); + return AjaxResult.success(); + } +} diff --git a/bashi-admin/src/main/java/com/bashi/web/controller/system/SysConfigController.java b/bashi-admin/src/main/java/com/bashi/web/controller/system/SysConfigController.java new file mode 100644 index 0000000..d5fb62c --- /dev/null +++ b/bashi-admin/src/main/java/com/bashi/web/controller/system/SysConfigController.java @@ -0,0 +1,128 @@ +package com.bashi.web.controller.system; + +import com.bashi.common.annotation.Log; +import com.bashi.common.annotation.RepeatSubmit; +import com.bashi.common.constant.UserConstants; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.enums.BusinessType; +import com.bashi.common.utils.SecurityUtils; +import com.bashi.common.utils.poi.ExcelUtil; +import com.bashi.system.domain.SysConfig; +import com.bashi.system.service.ISysConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 参数配置 信息操作处理 + * + * @author duteliang + */ +@RestController +@RequestMapping("/system/config") +public class SysConfigController extends BaseController +{ + @Autowired + private ISysConfigService configService; + + /** + * 获取参数配置列表 + */ + @PreAuthorize("@ss.hasPermi('system:config:list')") + @GetMapping("/list") + public TableDataInfo list(SysConfig config) + { + return configService.selectPageConfigList(config); + } + + @Log(title = "参数管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:config:export')") + @GetMapping("/export") + public AjaxResult export(SysConfig config) + { + List list = configService.selectConfigList(config); + ExcelUtil util = new ExcelUtil(SysConfig.class); + return util.exportExcel(list, "参数数据"); + } + + /** + * 根据参数编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:config:query')") + @GetMapping(value = "/{configId}") + public AjaxResult getInfo(@PathVariable Long configId) + { + return AjaxResult.success(configService.selectConfigById(configId)); + } + + /** + * 根据参数键名查询参数值 + */ + @GetMapping(value = "/configKey/{configKey}") + public AjaxResult getConfigKey(@PathVariable String configKey) + { + return AjaxResult.success(configService.selectConfigByKey(configKey)); + } + + /** + * 新增参数配置 + */ + @PreAuthorize("@ss.hasPermi('system:config:add')") + @Log(title = "参数管理", businessType = BusinessType.INSERT) + @PostMapping + @RepeatSubmit + public AjaxResult add(@Validated @RequestBody SysConfig config) + { + if (UserConstants.NOT_UNIQUE.equals(configService.checkConfigKeyUnique(config))) + { + return AjaxResult.error("新增参数'" + config.getConfigName() + "'失败,参数键名已存在"); + } + config.setCreateBy(SecurityUtils.getUsername()); + return toAjax(configService.insertConfig(config)); + } + + /** + * 修改参数配置 + */ + @PreAuthorize("@ss.hasPermi('system:config:edit')") + @Log(title = "参数管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysConfig config) + { + if (UserConstants.NOT_UNIQUE.equals(configService.checkConfigKeyUnique(config))) + { + return AjaxResult.error("修改参数'" + config.getConfigName() + "'失败,参数键名已存在"); + } + config.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(configService.updateConfig(config)); + } + + /** + * 删除参数配置 + */ + @PreAuthorize("@ss.hasPermi('system:config:remove')") + @Log(title = "参数管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{configIds}") + public AjaxResult remove(@PathVariable Long[] configIds) + { + configService.deleteConfigByIds(configIds); + return success(); + } + + /** + * 刷新参数缓存 + */ + @PreAuthorize("@ss.hasPermi('system:config:remove')") + @Log(title = "参数管理", businessType = BusinessType.CLEAN) + @DeleteMapping("/refreshCache") + public AjaxResult refreshCache() + { + configService.resetConfigCache(); + return AjaxResult.success(); + } +} diff --git a/bashi-admin/src/main/java/com/bashi/web/controller/system/SysDeptController.java b/bashi-admin/src/main/java/com/bashi/web/controller/system/SysDeptController.java new file mode 100644 index 0000000..b999686 --- /dev/null +++ b/bashi-admin/src/main/java/com/bashi/web/controller/system/SysDeptController.java @@ -0,0 +1,159 @@ +package com.bashi.web.controller.system; + +import cn.hutool.core.util.StrUtil; +import com.bashi.common.annotation.Log; +import com.bashi.common.constant.UserConstants; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.domain.entity.SysDept; +import com.bashi.common.enums.BusinessType; +import com.bashi.common.utils.SecurityUtils; +import com.bashi.system.service.ISysDeptService; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * 部门信息 + * + * @author duteliang + */ +@RestController +@RequestMapping("/system/dept") +public class SysDeptController extends BaseController +{ + @Autowired + private ISysDeptService deptService; + + /** + * 获取部门列表 + */ + @PreAuthorize("@ss.hasPermi('system:dept:list')") + @GetMapping("/list") + public AjaxResult list(SysDept dept) + { + List depts = deptService.selectDeptList(dept); + return AjaxResult.success(depts); + } + + /** + * 查询部门列表(排除节点) + */ + @PreAuthorize("@ss.hasPermi('system:dept:list')") + @GetMapping("/list/exclude/{deptId}") + public AjaxResult excludeChild(@PathVariable(value = "deptId", required = false) Long deptId) + { + List depts = deptService.selectDeptList(new SysDept()); + Iterator it = depts.iterator(); + while (it.hasNext()) + { + SysDept d = (SysDept) it.next(); + if (d.getDeptId().intValue() == deptId + || ArrayUtils.contains(StrUtil.splitToArray(d.getAncestors(), ','), deptId + "")) + { + it.remove(); + } + } + return AjaxResult.success(depts); + } + + /** + * 根据部门编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:dept:query')") + @GetMapping(value = "/{deptId}") + public AjaxResult getInfo(@PathVariable Long deptId) + { + return AjaxResult.success(deptService.selectDeptById(deptId)); + } + + /** + * 获取部门下拉树列表 + */ + @GetMapping("/treeselect") + public AjaxResult treeselect(SysDept dept) + { + List depts = deptService.selectDeptList(dept); + return AjaxResult.success(deptService.buildDeptTreeSelect(depts)); + } + + /** + * 加载对应角色部门列表树 + */ + @GetMapping(value = "/roleDeptTreeselect/{roleId}") + public AjaxResult roleDeptTreeselect(@PathVariable("roleId") Long roleId) + { + List depts = deptService.selectDeptList(new SysDept()); + Map ajax = new HashMap<>(); + ajax.put("checkedKeys", deptService.selectDeptListByRoleId(roleId)); + ajax.put("depts", deptService.buildDeptTreeSelect(depts)); + return AjaxResult.success(ajax); + } + + /** + * 新增部门 + */ + @PreAuthorize("@ss.hasPermi('system:dept:add')") + @Log(title = "部门管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysDept dept) + { + if (UserConstants.NOT_UNIQUE.equals(deptService.checkDeptNameUnique(dept))) + { + return AjaxResult.error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在"); + } + dept.setCreateBy(SecurityUtils.getUsername()); + return toAjax(deptService.insertDept(dept)); + } + + /** + * 修改部门 + */ + @PreAuthorize("@ss.hasPermi('system:dept:edit')") + @Log(title = "部门管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysDept dept) + { + if (UserConstants.NOT_UNIQUE.equals(deptService.checkDeptNameUnique(dept))) + { + return AjaxResult.error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在"); + } + else if (dept.getParentId().equals(dept.getDeptId())) + { + return AjaxResult.error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己"); + } + else if (StrUtil.equals(UserConstants.DEPT_DISABLE, dept.getStatus()) + && deptService.selectNormalChildrenDeptById(dept.getDeptId()) > 0) + { + return AjaxResult.error("该部门包含未停用的子部门!"); + } + dept.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(deptService.updateDept(dept)); + } + + /** + * 删除部门 + */ + @PreAuthorize("@ss.hasPermi('system:dept:remove')") + @Log(title = "部门管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{deptId}") + public AjaxResult remove(@PathVariable Long deptId) + { + if (deptService.hasChildByDeptId(deptId)) + { + return AjaxResult.error("存在下级部门,不允许删除"); + } + if (deptService.checkDeptExistUser(deptId)) + { + return AjaxResult.error("部门存在用户,不允许删除"); + } + return toAjax(deptService.deleteDeptById(deptId)); + } +} diff --git a/bashi-admin/src/main/java/com/bashi/web/controller/system/SysDictDataController.java b/bashi-admin/src/main/java/com/bashi/web/controller/system/SysDictDataController.java new file mode 100644 index 0000000..6f99c53 --- /dev/null +++ b/bashi-admin/src/main/java/com/bashi/web/controller/system/SysDictDataController.java @@ -0,0 +1,113 @@ +package com.bashi.web.controller.system; + +import cn.hutool.core.lang.Validator; +import com.bashi.common.annotation.Log; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.domain.entity.SysDictData; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.enums.BusinessType; +import com.bashi.common.utils.SecurityUtils; +import com.bashi.common.utils.poi.ExcelUtil; +import com.bashi.system.service.ISysDictDataService; +import com.bashi.system.service.ISysDictTypeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * 数据字典信息 + * + * @author duteliang + */ +@RestController +@RequestMapping("/system/dict/data") +public class SysDictDataController extends BaseController +{ + @Autowired + private ISysDictDataService dictDataService; + + @Autowired + private ISysDictTypeService dictTypeService; + + @PreAuthorize("@ss.hasPermi('system:dict:list')") + @GetMapping("/list") + public TableDataInfo list(SysDictData dictData) + { + return dictDataService.selectPageDictDataList(dictData); + } + + @Log(title = "字典数据", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:dict:export')") + @GetMapping("/export") + public AjaxResult export(SysDictData dictData) + { + List list = dictDataService.selectDictDataList(dictData); + ExcelUtil util = new ExcelUtil(SysDictData.class); + return util.exportExcel(list, "字典数据"); + } + + /** + * 查询字典数据详细 + */ + @PreAuthorize("@ss.hasPermi('system:dict:query')") + @GetMapping(value = "/{dictCode}") + public AjaxResult getInfo(@PathVariable Long dictCode) + { + return AjaxResult.success(dictDataService.selectDictDataById(dictCode)); + } + + /** + * 根据字典类型查询字典数据信息 + */ + @GetMapping(value = "/type/{dictType}") + public AjaxResult dictType(@PathVariable String dictType) + { + List data = dictTypeService.selectDictDataByType(dictType); + if (Validator.isNull(data)) + { + data = new ArrayList(); + } + return AjaxResult.success(data); + } + + /** + * 新增字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:add')") + @Log(title = "字典数据", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysDictData dict) + { + dict.setCreateBy(SecurityUtils.getUsername()); + return toAjax(dictDataService.insertDictData(dict)); + } + + /** + * 修改保存字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:edit')") + @Log(title = "字典数据", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysDictData dict) + { + dict.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(dictDataService.updateDictData(dict)); + } + + /** + * 删除字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:remove')") + @Log(title = "字典类型", businessType = BusinessType.DELETE) + @DeleteMapping("/{dictCodes}") + public AjaxResult remove(@PathVariable Long[] dictCodes) + { + dictDataService.deleteDictDataByIds(dictCodes); + return success(); + } +} diff --git a/bashi-admin/src/main/java/com/bashi/web/controller/system/SysDictTypeController.java b/bashi-admin/src/main/java/com/bashi/web/controller/system/SysDictTypeController.java new file mode 100644 index 0000000..6b3fed8 --- /dev/null +++ b/bashi-admin/src/main/java/com/bashi/web/controller/system/SysDictTypeController.java @@ -0,0 +1,124 @@ +package com.bashi.web.controller.system; + +import com.bashi.common.annotation.Log; +import com.bashi.common.constant.UserConstants; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.domain.entity.SysDictType; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.enums.BusinessType; +import com.bashi.common.utils.SecurityUtils; +import com.bashi.common.utils.poi.ExcelUtil; +import com.bashi.system.service.ISysDictTypeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 数据字典信息 + * + * @author duteliang + */ +@RestController +@RequestMapping("/system/dict/type") +public class SysDictTypeController extends BaseController +{ + @Autowired + private ISysDictTypeService dictTypeService; + + @PreAuthorize("@ss.hasPermi('system:dict:list')") + @GetMapping("/list") + public TableDataInfo list(SysDictType dictType) + { + return dictTypeService.selectPageDictTypeList(dictType); + } + + @Log(title = "字典类型", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:dict:export')") + @GetMapping("/export") + public AjaxResult export(SysDictType dictType) + { + List list = dictTypeService.selectDictTypeList(dictType); + ExcelUtil util = new ExcelUtil(SysDictType.class); + return util.exportExcel(list, "字典类型"); + } + + /** + * 查询字典类型详细 + */ + @PreAuthorize("@ss.hasPermi('system:dict:query')") + @GetMapping(value = "/{dictId}") + public AjaxResult getInfo(@PathVariable Long dictId) + { + return AjaxResult.success(dictTypeService.selectDictTypeById(dictId)); + } + + /** + * 新增字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:add')") + @Log(title = "字典类型", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysDictType dict) + { + if (UserConstants.NOT_UNIQUE.equals(dictTypeService.checkDictTypeUnique(dict))) + { + return AjaxResult.error("新增字典'" + dict.getDictName() + "'失败,字典类型已存在"); + } + dict.setCreateBy(SecurityUtils.getUsername()); + return toAjax(dictTypeService.insertDictType(dict)); + } + + /** + * 修改字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:edit')") + @Log(title = "字典类型", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysDictType dict) + { + if (UserConstants.NOT_UNIQUE.equals(dictTypeService.checkDictTypeUnique(dict))) + { + return AjaxResult.error("修改字典'" + dict.getDictName() + "'失败,字典类型已存在"); + } + dict.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(dictTypeService.updateDictType(dict)); + } + + /** + * 删除字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:remove')") + @Log(title = "字典类型", businessType = BusinessType.DELETE) + @DeleteMapping("/{dictIds}") + public AjaxResult remove(@PathVariable Long[] dictIds) + { + dictTypeService.deleteDictTypeByIds(dictIds); + return success(); + } + + /** + * 刷新字典缓存 + */ + @PreAuthorize("@ss.hasPermi('system:dict:remove')") + @Log(title = "字典类型", businessType = BusinessType.CLEAN) + @DeleteMapping("/refreshCache") + public AjaxResult refreshCache() + { + dictTypeService.resetDictCache(); + return AjaxResult.success(); + } + + /** + * 获取字典选择框列表 + */ + @GetMapping("/optionselect") + public AjaxResult optionselect() + { + List dictTypes = dictTypeService.selectDictTypeAll(); + return AjaxResult.success(dictTypes); + } +} diff --git a/bashi-admin/src/main/java/com/bashi/web/controller/system/SysLoginController.java b/bashi-admin/src/main/java/com/bashi/web/controller/system/SysLoginController.java new file mode 100644 index 0000000..61c8776 --- /dev/null +++ b/bashi-admin/src/main/java/com/bashi/web/controller/system/SysLoginController.java @@ -0,0 +1,93 @@ +package com.bashi.web.controller.system; + +import com.bashi.common.constant.Constants; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.domain.entity.SysMenu; +import com.bashi.common.core.domain.entity.SysUser; +import com.bashi.common.core.domain.model.LoginBody; +import com.bashi.common.core.domain.model.LoginUser; +import com.bashi.common.utils.ServletUtils; +import com.bashi.framework.web.service.SysLoginService; +import com.bashi.framework.web.service.SysPermissionService; +import com.bashi.framework.web.service.TokenService; +import com.bashi.system.service.ISysMenuService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 登录验证 + * + * @author duteliang + */ +@RestController +public class SysLoginController { + @Autowired + private SysLoginService loginService; + + @Autowired + private ISysMenuService menuService; + + @Autowired + private SysPermissionService permissionService; + + @Autowired + private TokenService tokenService; + + /** + * 登录方法 + * + * @param loginBody 登录信息 + * @return 结果 + */ + @PostMapping("/login") + public AjaxResult login(@RequestBody LoginBody loginBody) { + Map ajax = new HashMap<>(); + // 生成令牌 + String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(), + loginBody.getUuid()); + ajax.put(Constants.TOKEN, token); + return AjaxResult.success(ajax); + } + + /** + * 获取用户信息 + * + * @return 用户信息 + */ + @GetMapping("getInfo") + public AjaxResult getInfo() { + LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest()); + SysUser user = loginUser.getUser(); + // 角色集合 + Set roles = permissionService.getRolePermission(user); + // 权限集合 + Set permissions = permissionService.getMenuPermission(user); + Map ajax = new HashMap<>(); + ajax.put("user", user); + ajax.put("roles", roles); + ajax.put("permissions", permissions); + return AjaxResult.success(ajax); + } + + /** + * 获取路由信息 + * + * @return 路由信息 + */ + @GetMapping("getRouters") + public AjaxResult getRouters() { + LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest()); + // 用户信息 + SysUser user = loginUser.getUser(); + List menus = menuService.selectMenuTreeByUserId(user.getUserId()); + return AjaxResult.success(menuService.buildMenus(menus)); + } +} diff --git a/bashi-admin/src/main/java/com/bashi/web/controller/system/SysMenuController.java b/bashi-admin/src/main/java/com/bashi/web/controller/system/SysMenuController.java new file mode 100644 index 0000000..4b8f88a --- /dev/null +++ b/bashi-admin/src/main/java/com/bashi/web/controller/system/SysMenuController.java @@ -0,0 +1,153 @@ +package com.bashi.web.controller.system; + +import cn.hutool.core.util.StrUtil; +import com.bashi.common.annotation.Log; +import com.bashi.common.constant.Constants; +import com.bashi.common.constant.UserConstants; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.domain.entity.SysMenu; +import com.bashi.common.core.domain.model.LoginUser; +import com.bashi.common.enums.BusinessType; +import com.bashi.common.utils.SecurityUtils; +import com.bashi.common.utils.ServletUtils; +import com.bashi.framework.web.service.TokenService; +import com.bashi.system.service.ISysMenuService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 菜单信息 + * + * @author duteliang + */ +@RestController +@RequestMapping("/system/menu") +public class SysMenuController extends BaseController +{ + @Autowired + private ISysMenuService menuService; + + @Autowired + private TokenService tokenService; + + /** + * 获取菜单列表 + */ + @PreAuthorize("@ss.hasPermi('system:menu:list')") + @GetMapping("/list") + public AjaxResult list(SysMenu menu) + { + LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest()); + Long userId = loginUser.getUser().getUserId(); + List menus = menuService.selectMenuList(menu, userId); + return AjaxResult.success(menus); + } + + /** + * 根据菜单编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:menu:query')") + @GetMapping(value = "/{menuId}") + public AjaxResult getInfo(@PathVariable Long menuId) + { + return AjaxResult.success(menuService.selectMenuById(menuId)); + } + + /** + * 获取菜单下拉树列表 + */ + @GetMapping("/treeselect") + public AjaxResult treeselect(SysMenu menu) + { + LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest()); + Long userId = loginUser.getUser().getUserId(); + List menus = menuService.selectMenuList(menu, userId); + return AjaxResult.success(menuService.buildMenuTreeSelect(menus)); + } + + /** + * 加载对应角色菜单列表树 + */ + @GetMapping(value = "/roleMenuTreeselect/{roleId}") + public AjaxResult roleMenuTreeselect(@PathVariable("roleId") Long roleId) + { + LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest()); + List menus = menuService.selectMenuList(loginUser.getUser().getUserId()); + Map ajax = new HashMap<>(); + ajax.put("checkedKeys", menuService.selectMenuListByRoleId(roleId)); + ajax.put("menus", menuService.buildMenuTreeSelect(menus)); + return AjaxResult.success(ajax); + } + + /** + * 新增菜单 + */ + @PreAuthorize("@ss.hasPermi('system:menu:add')") + @Log(title = "菜单管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysMenu menu) + { + if (UserConstants.NOT_UNIQUE.equals(menuService.checkMenuNameUnique(menu))) + { + return AjaxResult.error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); + } + else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) + && !StrUtil.startWithAny(menu.getPath(), Constants.HTTP, Constants.HTTPS)) + { + return AjaxResult.error("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); + } + menu.setCreateBy(SecurityUtils.getUsername()); + return toAjax(menuService.insertMenu(menu)); + } + + /** + * 修改菜单 + */ + @PreAuthorize("@ss.hasPermi('system:menu:edit')") + @Log(title = "菜单管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysMenu menu) + { + if (UserConstants.NOT_UNIQUE.equals(menuService.checkMenuNameUnique(menu))) + { + return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); + } + else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) + && !StrUtil.startWithAny(menu.getPath(), Constants.HTTP, Constants.HTTPS)) + { + return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); + } + else if (menu.getMenuId().equals(menu.getParentId())) + { + return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己"); + } + menu.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(menuService.updateMenu(menu)); + } + + /** + * 删除菜单 + */ + @PreAuthorize("@ss.hasPermi('system:menu:remove')") + @Log(title = "菜单管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{menuId}") + public AjaxResult remove(@PathVariable("menuId") Long menuId) + { + if (menuService.hasChildByMenuId(menuId)) + { + return AjaxResult.error("存在子菜单,不允许删除"); + } + if (menuService.checkMenuExistRole(menuId)) + { + return AjaxResult.error("菜单已分配,不允许删除"); + } + return toAjax(menuService.deleteMenuById(menuId)); + } +} diff --git a/bashi-admin/src/main/java/com/bashi/web/controller/system/SysNoticeController.java b/bashi-admin/src/main/java/com/bashi/web/controller/system/SysNoticeController.java new file mode 100644 index 0000000..2d328ef --- /dev/null +++ b/bashi-admin/src/main/java/com/bashi/web/controller/system/SysNoticeController.java @@ -0,0 +1,89 @@ +package com.bashi.web.controller.system; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.bashi.common.annotation.Log; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.enums.BusinessType; +import com.bashi.common.utils.SecurityUtils; +import com.bashi.system.domain.SysNotice; +import com.bashi.system.service.ISysNoticeService; + +/** + * 公告 信息操作处理 + * + * @author duteliang + */ +@RestController +@RequestMapping("/system/notice") +public class SysNoticeController extends BaseController +{ + @Autowired + private ISysNoticeService noticeService; + + /** + * 获取通知公告列表 + */ + @PreAuthorize("@ss.hasPermi('system:notice:list')") + @GetMapping("/list") + public TableDataInfo list(SysNotice notice) + { + return noticeService.selectPageNoticeList(notice); + } + + /** + * 根据通知公告编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:notice:query')") + @GetMapping(value = "/{noticeId}") + public AjaxResult getInfo(@PathVariable Long noticeId) + { + return AjaxResult.success(noticeService.selectNoticeById(noticeId)); + } + + /** + * 新增通知公告 + */ + @PreAuthorize("@ss.hasPermi('system:notice:add')") + @Log(title = "通知公告", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysNotice notice) + { + notice.setCreateBy(SecurityUtils.getUsername()); + return toAjax(noticeService.insertNotice(notice)); + } + + /** + * 修改通知公告 + */ + @PreAuthorize("@ss.hasPermi('system:notice:edit')") + @Log(title = "通知公告", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysNotice notice) + { + notice.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(noticeService.updateNotice(notice)); + } + + /** + * 删除通知公告 + */ + @PreAuthorize("@ss.hasPermi('system:notice:remove')") + @Log(title = "通知公告", businessType = BusinessType.DELETE) + @DeleteMapping("/{noticeIds}") + public AjaxResult remove(@PathVariable Long[] noticeIds) + { + return toAjax(noticeService.deleteNoticeByIds(noticeIds)); + } +} diff --git a/bashi-admin/src/main/java/com/bashi/web/controller/system/SysPostController.java b/bashi-admin/src/main/java/com/bashi/web/controller/system/SysPostController.java new file mode 100644 index 0000000..d81ac01 --- /dev/null +++ b/bashi-admin/src/main/java/com/bashi/web/controller/system/SysPostController.java @@ -0,0 +1,128 @@ +package com.bashi.web.controller.system; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.bashi.common.annotation.Log; +import com.bashi.common.constant.UserConstants; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.enums.BusinessType; +import com.bashi.common.utils.SecurityUtils; +import com.bashi.common.utils.poi.ExcelUtil; +import com.bashi.system.domain.SysPost; +import com.bashi.system.service.ISysPostService; + +/** + * 岗位信息操作处理 + * + * @author duteliang + */ +@RestController +@RequestMapping("/system/post") +public class SysPostController extends BaseController +{ + @Autowired + private ISysPostService postService; + + /** + * 获取岗位列表 + */ + @PreAuthorize("@ss.hasPermi('system:post:list')") + @GetMapping("/list") + public TableDataInfo list(SysPost post) + { + return postService.selectPagePostList(post); + } + + @Log(title = "岗位管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:post:export')") + @GetMapping("/export") + public AjaxResult export(SysPost post) + { + List list = postService.selectPostList(post); + ExcelUtil util = new ExcelUtil(SysPost.class); + return util.exportExcel(list, "岗位数据"); + } + + /** + * 根据岗位编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:post:query')") + @GetMapping(value = "/{postId}") + public AjaxResult getInfo(@PathVariable Long postId) + { + return AjaxResult.success(postService.selectPostById(postId)); + } + + /** + * 新增岗位 + */ + @PreAuthorize("@ss.hasPermi('system:post:add')") + @Log(title = "岗位管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysPost post) + { + if (UserConstants.NOT_UNIQUE.equals(postService.checkPostNameUnique(post))) + { + return AjaxResult.error("新增岗位'" + post.getPostName() + "'失败,岗位名称已存在"); + } + else if (UserConstants.NOT_UNIQUE.equals(postService.checkPostCodeUnique(post))) + { + return AjaxResult.error("新增岗位'" + post.getPostName() + "'失败,岗位编码已存在"); + } + post.setCreateBy(SecurityUtils.getUsername()); + return toAjax(postService.insertPost(post)); + } + + /** + * 修改岗位 + */ + @PreAuthorize("@ss.hasPermi('system:post:edit')") + @Log(title = "岗位管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysPost post) + { + if (UserConstants.NOT_UNIQUE.equals(postService.checkPostNameUnique(post))) + { + return AjaxResult.error("修改岗位'" + post.getPostName() + "'失败,岗位名称已存在"); + } + else if (UserConstants.NOT_UNIQUE.equals(postService.checkPostCodeUnique(post))) + { + return AjaxResult.error("修改岗位'" + post.getPostName() + "'失败,岗位编码已存在"); + } + post.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(postService.updatePost(post)); + } + + /** + * 删除岗位 + */ + @PreAuthorize("@ss.hasPermi('system:post:remove')") + @Log(title = "岗位管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{postIds}") + public AjaxResult remove(@PathVariable Long[] postIds) + { + return toAjax(postService.deletePostByIds(postIds)); + } + + /** + * 获取岗位选择框列表 + */ + @GetMapping("/optionselect") + public AjaxResult optionselect() + { + List posts = postService.selectPostAll(); + return AjaxResult.success(posts); + } +} diff --git a/bashi-admin/src/main/java/com/bashi/web/controller/system/SysProfileController.java b/bashi-admin/src/main/java/com/bashi/web/controller/system/SysProfileController.java new file mode 100644 index 0000000..df95f50 --- /dev/null +++ b/bashi-admin/src/main/java/com/bashi/web/controller/system/SysProfileController.java @@ -0,0 +1,137 @@ +package com.bashi.web.controller.system; + +import cn.hutool.core.util.StrUtil; +import com.bashi.common.annotation.Log; +import com.bashi.common.config.BsConfig; +import com.bashi.common.constant.UserConstants; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.domain.entity.SysUser; +import com.bashi.common.core.domain.model.LoginUser; +import com.bashi.common.enums.BusinessType; +import com.bashi.common.utils.SecurityUtils; +import com.bashi.common.utils.ServletUtils; +import com.bashi.common.utils.file.FileUploadUtils; +import com.bashi.framework.web.service.TokenService; +import com.bashi.system.service.ISysUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * 个人信息 业务处理 + * + * @author duteliang + */ +@RestController +@RequestMapping("/system/user/profile") +public class SysProfileController extends BaseController +{ + @Autowired + private ISysUserService userService; + + @Autowired + private TokenService tokenService; + + /** + * 个人信息 + */ + @GetMapping + public AjaxResult profile() + { + LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest()); + SysUser user = loginUser.getUser(); + Map ajax = new HashMap<>(); + ajax.put("user", user); + ajax.put("roleGroup", userService.selectUserRoleGroup(loginUser.getUsername())); + ajax.put("postGroup", userService.selectUserPostGroup(loginUser.getUsername())); + return AjaxResult.success(ajax); + } + + /** + * 修改用户 + */ + @Log(title = "个人信息", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult updateProfile(@RequestBody SysUser user) + { + if (StrUtil.isNotEmpty(user.getPhonenumber()) + && UserConstants.NOT_UNIQUE.equals(userService.checkPhoneUnique(user))) + { + return AjaxResult.error("修改用户'" + user.getUserName() + "'失败,手机号码已存在"); + } + if (StrUtil.isNotEmpty(user.getEmail()) + && UserConstants.NOT_UNIQUE.equals(userService.checkEmailUnique(user))) + { + return AjaxResult.error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在"); + } + if (userService.updateUserProfile(user) > 0) + { + LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest()); + // 更新缓存用户信息 + loginUser.getUser().setNickName(user.getNickName()); + loginUser.getUser().setPhonenumber(user.getPhonenumber()); + loginUser.getUser().setEmail(user.getEmail()); + loginUser.getUser().setSex(user.getSex()); + tokenService.setLoginUser(loginUser); + return AjaxResult.success(); + } + return AjaxResult.error("修改个人信息异常,请联系管理员"); + } + + /** + * 重置密码 + */ + @Log(title = "个人信息", businessType = BusinessType.UPDATE) + @PutMapping("/updatePwd") + public AjaxResult updatePwd(String oldPassword, String newPassword) + { + LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest()); + String userName = loginUser.getUsername(); + String password = loginUser.getPassword(); + if (!SecurityUtils.matchesPassword(oldPassword, password)) + { + return AjaxResult.error("修改密码失败,旧密码错误"); + } + if (SecurityUtils.matchesPassword(newPassword, password)) + { + return AjaxResult.error("新密码不能与旧密码相同"); + } + if (userService.resetUserPwd(userName, SecurityUtils.encryptPassword(newPassword)) > 0) + { + // 更新缓存用户密码 + loginUser.getUser().setPassword(SecurityUtils.encryptPassword(newPassword)); + tokenService.setLoginUser(loginUser); + return AjaxResult.success(); + } + return AjaxResult.error("修改密码异常,请联系管理员"); + } + + /** + * 头像上传 + */ + @Log(title = "用户头像", businessType = BusinessType.UPDATE) + @PostMapping("/avatar") + public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws IOException + { + if (!file.isEmpty()) + { + LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest()); + String avatar = FileUploadUtils.upload(BsConfig.getAvatarPath(), file); + if (userService.updateUserAvatar(loginUser.getUsername(), avatar)) + { + Map ajax = new HashMap<>(); + ajax.put("imgUrl", avatar); + // 更新缓存用户头像 + loginUser.getUser().setAvatar(avatar); + tokenService.setLoginUser(loginUser); + return AjaxResult.success(ajax); + } + } + return AjaxResult.error("上传图片异常,请联系管理员"); + } +} diff --git a/bashi-admin/src/main/java/com/bashi/web/controller/system/SysRoleController.java b/bashi-admin/src/main/java/com/bashi/web/controller/system/SysRoleController.java new file mode 100644 index 0000000..d5cbd3f --- /dev/null +++ b/bashi-admin/src/main/java/com/bashi/web/controller/system/SysRoleController.java @@ -0,0 +1,174 @@ +package com.bashi.web.controller.system; + +import cn.hutool.core.lang.Validator; +import com.bashi.common.annotation.Log; +import com.bashi.common.constant.UserConstants; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.domain.entity.SysRole; +import com.bashi.common.core.domain.model.LoginUser; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.enums.BusinessType; +import com.bashi.common.utils.SecurityUtils; +import com.bashi.common.utils.ServletUtils; +import com.bashi.common.utils.poi.ExcelUtil; +import com.bashi.framework.web.service.SysPermissionService; +import com.bashi.framework.web.service.TokenService; +import com.bashi.system.service.ISysRoleService; +import com.bashi.system.service.ISysUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 角色信息 + * + * @author duteliang + */ +@RestController +@RequestMapping("/system/role") +public class SysRoleController extends BaseController +{ + @Autowired + private ISysRoleService roleService; + + @Autowired + private TokenService tokenService; + + @Autowired + private SysPermissionService permissionService; + + @Autowired + private ISysUserService userService; + + @PreAuthorize("@ss.hasPermi('system:role:list')") + @GetMapping("/list") + public TableDataInfo list(SysRole role) + { + return roleService.selectPageRoleList(role); + } + + @Log(title = "角色管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:role:export')") + @GetMapping("/export") + public AjaxResult export(SysRole role) + { + List list = roleService.selectRoleList(role); + ExcelUtil util = new ExcelUtil(SysRole.class); + return util.exportExcel(list, "角色数据"); + } + + /** + * 根据角色编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:role:query')") + @GetMapping(value = "/{roleId}") + public AjaxResult getInfo(@PathVariable Long roleId) + { + return AjaxResult.success(roleService.selectRoleById(roleId)); + } + + /** + * 新增角色 + */ + @PreAuthorize("@ss.hasPermi('system:role:add')") + @Log(title = "角色管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysRole role) + { + if (UserConstants.NOT_UNIQUE.equals(roleService.checkRoleNameUnique(role))) + { + return AjaxResult.error("新增角色'" + role.getRoleName() + "'失败,角色名称已存在"); + } + else if (UserConstants.NOT_UNIQUE.equals(roleService.checkRoleKeyUnique(role))) + { + return AjaxResult.error("新增角色'" + role.getRoleName() + "'失败,角色权限已存在"); + } + role.setCreateBy(SecurityUtils.getUsername()); + return toAjax(roleService.insertRole(role)); + + } + + /** + * 修改保存角色 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysRole role) + { + roleService.checkRoleAllowed(role); + if (UserConstants.NOT_UNIQUE.equals(roleService.checkRoleNameUnique(role))) + { + return AjaxResult.error("修改角色'" + role.getRoleName() + "'失败,角色名称已存在"); + } + else if (UserConstants.NOT_UNIQUE.equals(roleService.checkRoleKeyUnique(role))) + { + return AjaxResult.error("修改角色'" + role.getRoleName() + "'失败,角色权限已存在"); + } + role.setUpdateBy(SecurityUtils.getUsername()); + + if (roleService.updateRole(role) > 0) + { + // 更新缓存用户权限 + LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest()); + if (Validator.isNotNull(loginUser.getUser()) && !loginUser.getUser().isAdmin()) + { + loginUser.setPermissions(permissionService.getMenuPermission(loginUser.getUser())); + loginUser.setUser(userService.selectUserByUserName(loginUser.getUser().getUserName())); + tokenService.setLoginUser(loginUser); + } + return AjaxResult.success(); + } + return AjaxResult.error("修改角色'" + role.getRoleName() + "'失败,请联系管理员"); + } + + /** + * 修改保存数据权限 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.UPDATE) + @PutMapping("/dataScope") + public AjaxResult dataScope(@RequestBody SysRole role) + { + roleService.checkRoleAllowed(role); + return toAjax(roleService.authDataScope(role)); + } + + /** + * 状态修改 + */ + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysRole role) + { + roleService.checkRoleAllowed(role); + role.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(roleService.updateRoleStatus(role)); + } + + /** + * 删除角色 + */ + @PreAuthorize("@ss.hasPermi('system:role:remove')") + @Log(title = "角色管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{roleIds}") + public AjaxResult remove(@PathVariable Long[] roleIds) + { + return toAjax(roleService.deleteRoleByIds(roleIds)); + } + + /** + * 获取角色选择框列表 + */ + @PreAuthorize("@ss.hasPermi('system:role:query')") + @GetMapping("/optionselect") + public AjaxResult optionselect() + { + return AjaxResult.success(roleService.selectRoleAll()); + } +} diff --git a/bashi-admin/src/main/java/com/bashi/web/controller/system/SysUserController.java b/bashi-admin/src/main/java/com/bashi/web/controller/system/SysUserController.java new file mode 100644 index 0000000..09cdb84 --- /dev/null +++ b/bashi-admin/src/main/java/com/bashi/web/controller/system/SysUserController.java @@ -0,0 +1,180 @@ +package com.bashi.web.controller.system; + +import cn.hutool.core.lang.Validator; +import com.bashi.common.annotation.Log; +import com.bashi.common.constant.UserConstants; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.domain.entity.SysRole; +import com.bashi.common.core.domain.entity.SysUser; +import com.bashi.common.core.domain.model.LoginUser; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.enums.BusinessType; +import com.bashi.common.utils.SecurityUtils; +import com.bashi.common.utils.ServletUtils; +import com.bashi.common.utils.poi.ExcelUtil; +import com.bashi.framework.web.service.TokenService; +import com.bashi.system.service.ISysPostService; +import com.bashi.system.service.ISysRoleService; +import com.bashi.system.service.ISysUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 用户信息 + * + * @author duteliang + */ +@RestController +@RequestMapping("/system/user") +public class SysUserController extends BaseController { + @Autowired + private ISysUserService userService; + + @Autowired + private ISysRoleService roleService; + + @Autowired + private ISysPostService postService; + + @Autowired + private TokenService tokenService; + + /** + * 获取用户列表 + */ + @PreAuthorize("@ss.hasPermi('system:user:list')") + @GetMapping("/list") + public TableDataInfo list(SysUser user) { + return userService.selectPageUserList(user); + } + + @Log(title = "用户管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:user:export')") + @GetMapping("/export") + public AjaxResult export(SysUser user) { + List list = userService.selectUserList(user); + ExcelUtil util = new ExcelUtil(SysUser.class); + return util.exportExcel(list, "用户数据"); + } + + @Log(title = "用户管理", businessType = BusinessType.IMPORT) + @PreAuthorize("@ss.hasPermi('system:user:import')") + @PostMapping("/importData") + public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception { + ExcelUtil util = new ExcelUtil(SysUser.class); + List userList = util.importExcel(file.getInputStream()); + LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest()); + String operName = loginUser.getUsername(); + String message = userService.importUser(userList, updateSupport, operName); + return AjaxResult.success(message); + } + + @GetMapping("/importTemplate") + public AjaxResult importTemplate() { + ExcelUtil util = new ExcelUtil(SysUser.class); + return util.importTemplateExcel("用户数据"); + } + + /** + * 根据用户编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('system:user:query')") + @GetMapping(value = {"/", "/{userId}"}) + public AjaxResult getInfo(@PathVariable(value = "userId", required = false) Long userId) { + Map ajax = new HashMap<>(); + List roles = roleService.selectRoleAll(); + ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList())); + ajax.put("posts", postService.selectPostAll()); + if (Validator.isNotNull(userId)) { + ajax.put("user", userService.selectUserById(userId)); + ajax.put("postIds", postService.selectPostListByUserId(userId)); + ajax.put("roleIds", roleService.selectRoleListByUserId(userId)); + } + return AjaxResult.success(ajax); + } + + /** + * 新增用户 + */ + @PreAuthorize("@ss.hasPermi('system:user:add')") + @Log(title = "用户管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysUser user) { + if (UserConstants.NOT_UNIQUE.equals(userService.checkUserNameUnique(user.getUserName()))) { + return AjaxResult.error("新增用户'" + user.getUserName() + "'失败,登录账号已存在"); + } else if (Validator.isNotEmpty(user.getPhonenumber()) + && UserConstants.NOT_UNIQUE.equals(userService.checkPhoneUnique(user))) { + return AjaxResult.error("新增用户'" + user.getUserName() + "'失败,手机号码已存在"); + } else if (Validator.isNotEmpty(user.getEmail()) + && UserConstants.NOT_UNIQUE.equals(userService.checkEmailUnique(user))) { + return AjaxResult.error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在"); + } + user.setCreateBy(SecurityUtils.getUsername()); + user.setPassword(SecurityUtils.encryptPassword(user.getPassword())); + return toAjax(userService.insertUser(user)); + } + + + /** + * 修改用户 + */ + @PreAuthorize("@ss.hasPermi('system:user:edit')") + @Log(title = "用户管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysUser user) { + userService.checkUserAllowed(user); + if (Validator.isNotEmpty(user.getPhonenumber()) + && UserConstants.NOT_UNIQUE.equals(userService.checkPhoneUnique(user))) { + return AjaxResult.error("修改用户'" + user.getUserName() + "'失败,手机号码已存在"); + } else if (Validator.isNotEmpty(user.getEmail()) + && UserConstants.NOT_UNIQUE.equals(userService.checkEmailUnique(user))) { + return AjaxResult.error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在"); + } + user.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(userService.updateUser(user)); + } + + /** + * 删除用户 + */ + @PreAuthorize("@ss.hasPermi('system:user:remove')") + @Log(title = "用户管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{userIds}") + public AjaxResult remove(@PathVariable Long[] userIds) { + return toAjax(userService.deleteUserByIds(userIds)); + } + + /** + * 重置密码 + */ + @PreAuthorize("@ss.hasPermi('system:user:resetPwd')") + @Log(title = "用户管理", businessType = BusinessType.UPDATE) + @PutMapping("/resetPwd") + public AjaxResult resetPwd(@RequestBody SysUser user) { + userService.checkUserAllowed(user); + user.setPassword(SecurityUtils.encryptPassword(user.getPassword())); + user.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(userService.resetPwd(user)); + } + + /** + * 状态修改 + */ + @PreAuthorize("@ss.hasPermi('system:user:edit')") + @Log(title = "用户管理", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysUser user) { + userService.checkUserAllowed(user); + user.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(userService.updateUserStatus(user)); + } +} diff --git a/bashi-admin/src/main/resources/META-INF/spring-devtools.properties b/bashi-admin/src/main/resources/META-INF/spring-devtools.properties new file mode 100644 index 0000000..2b23f85 --- /dev/null +++ b/bashi-admin/src/main/resources/META-INF/spring-devtools.properties @@ -0,0 +1 @@ +restart.include.json=/com.alibaba.fastjson.*.jar \ No newline at end of file diff --git a/bashi-admin/src/main/resources/application-dev.yml b/bashi-admin/src/main/resources/application-dev.yml new file mode 100644 index 0000000..fbca8c7 --- /dev/null +++ b/bashi-admin/src/main/resources/application-dev.yml @@ -0,0 +1,112 @@ +# 数据源配置 +spring: + autoconfigure: + exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure + datasource: + type: com.alibaba.druid.pool.DruidDataSource + # 动态数据源文档 https://www.kancloud.cn/tracy5546/dynamic-datasource/content + dynamic: + #设置默认的数据源或者数据源组,默认值即为 master + primary: master + datasource: + # 主库数据源 + master: + driverClassName: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:4306/dk?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true + username: dk + password: dk123.com + # 从库数据源 + slave: + driverClassName: com.mysql.cj.jdbc.Driver + url: + username: + password: + druid: + # 初始连接数 + initialSize: 5 + # 最小连接池数量 + minIdle: 10 + # 最大连接池数量 + maxActive: 20 + # 配置获取连接等待超时的时间 + maxWait: 60000 + # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 + timeBetweenEvictionRunsMillis: 60000 + # 配置一个连接在池中最小生存的时间,单位是毫秒 + minEvictableIdleTimeMillis: 300000 + # 配置一个连接在池中最大生存的时间,单位是毫秒 + maxEvictableIdleTimeMillis: 900000 + # 配置检测连接是否有效 + validationQuery: SELECT 1 FROM DUAL + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + # 注意这个值和druid原生不一致,默认启动了stat + filters: stat + webStatFilter: + enabled: false + statViewServlet: + enabled: false + # 设置白名单,不填则允许所有访问 + allow: + url-pattern: /druid/* + # 控制台管理用户名和密码 + login-username: duteliang + login-password: 123456 + filter: + stat: + enabled: false + # 慢SQL记录 + log-slow-sql: true + slow-sql-millis: 1000 + merge-sql: true + wall: + config: + multi-statement-allow: true + # redis 配置 + redis: + # 地址 + host: localhost + # 端口,默认为6379 + port: 6379 + # 数据库索引 + database: 1 + # 密码 + password: + # 连接超时时间 + timeout: 10s + # 是否开启ssl + ssl: false + +--- # redisson 客户端配置 +redisson: + # 线程池数量 + threads: 16 + # Netty线程池数量 + nettyThreads: 32 + # 传输模式 + transportMode: "NIO" + # 单节点配置 + singleServerConfig: + # 客户端名称 + clientName: ${bashi.name} + # 最小空闲连接数 + connectionMinimumIdleSize: 32 + # 连接池大小 + connectionPoolSize: 64 + # 连接空闲超时,单位:毫秒 + idleConnectionTimeout: 10000 + # 命令等待超时,单位:毫秒 + timeout: 3000 + # 如果尝试在此限制之内发送成功,则开始启用 timeout 计时。 + retryAttempts: 3 + # 命令重试发送时间间隔,单位:毫秒 + retryInterval: 1500 + # 发布和订阅连接的最小空闲连接数 + subscriptionConnectionMinimumIdleSize: 1 + # 发布和订阅连接池大小 + subscriptionConnectionPoolSize: 50 + # 单个连接最大订阅数量 + subscriptionsPerConnection: 5 + # DNS监测时间间隔,单位:毫秒 + dnsMonitoringInterval: 5000 diff --git a/bashi-admin/src/main/resources/application-prod.yml b/bashi-admin/src/main/resources/application-prod.yml new file mode 100644 index 0000000..ccb7da5 --- /dev/null +++ b/bashi-admin/src/main/resources/application-prod.yml @@ -0,0 +1,112 @@ +# 数据源配置 +spring: + autoconfigure: + exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure + datasource: + type: com.alibaba.druid.pool.DruidDataSource + # 动态数据源文档 https://www.kancloud.cn/tracy5546/dynamic-datasource/content + dynamic: + #设置默认的数据源或者数据源组,默认值即为 master + primary: master + datasource: + # 主库数据源 + master: + driverClassName: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:4306/dk?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true + username: dk + password: dk + # 从库数据源 + slave: + driverClassName: com.mysql.cj.jdbc.Driver + url: + username: + password: + druid: + # 初始连接数 + initialSize: 5 + # 最小连接池数量 + minIdle: 10 + # 最大连接池数量 + maxActive: 20 + # 配置获取连接等待超时的时间 + maxWait: 60000 + # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 + timeBetweenEvictionRunsMillis: 60000 + # 配置一个连接在池中最小生存的时间,单位是毫秒 + minEvictableIdleTimeMillis: 300000 + # 配置一个连接在池中最大生存的时间,单位是毫秒 + maxEvictableIdleTimeMillis: 900000 + # 配置检测连接是否有效 + validationQuery: SELECT 1 FROM DUAL + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + # 注意这个值和druid原生不一致,默认启动了stat + filters: stat + webStatFilter: + enabled: false + statViewServlet: + enabled: false + # 设置白名单,不填则允许所有访问 + allow: + url-pattern: /druid/* + # 控制台管理用户名和密码 + login-username: duteliang + login-password: 123456 + filter: + stat: + enabled: false + # 慢SQL记录 + log-slow-sql: true + slow-sql-millis: 1000 + merge-sql: true + wall: + config: + multi-statement-allow: true + # redis 配置 + redis: + # 地址 + host: localhost + # 端口,默认为 6379 + port: 9379 + # 数据库索引 + database: 3 + # 密码 + password: 383200134 + # 连接超时时间 + timeout: 10s + # 是否开启ssl + ssl: false + +--- # redisson 客户端配置 +redisson: + # 线程池数量 + threads: 16 + # Netty线程池数量 + nettyThreads: 32 + # 传输模式 + transportMode: "NIO" + # 单节点配置 + singleServerConfig: + # 客户端名称 + clientName: ${duteliang.name} + # 最小空闲连接数 + connectionMinimumIdleSize: 32 + # 连接池大小 + connectionPoolSize: 64 + # 连接空闲超时,单位:毫秒 + idleConnectionTimeout: 10000 + # 命令等待超时,单位:毫秒 + timeout: 3000 + # 如果尝试在此限制之内发送成功,则开始启用 timeout 计时。 + retryAttempts: 3 + # 命令重试发送时间间隔,单位:毫秒 + retryInterval: 1500 + # 发布和订阅连接的最小空闲连接数 + subscriptionConnectionMinimumIdleSize: 1 + # 发布和订阅连接池大小 + subscriptionConnectionPoolSize: 50 + # 单个连接最大订阅数量 + subscriptionsPerConnection: 5 + # DNS监测时间间隔,单位:毫秒 + dnsMonitoringInterval: 5000 diff --git a/bashi-admin/src/main/resources/application.yml b/bashi-admin/src/main/resources/application.yml new file mode 100644 index 0000000..28955f5 --- /dev/null +++ b/bashi-admin/src/main/resources/application.yml @@ -0,0 +1,340 @@ +# 项目相关配置 +bashi: + # 名称 + name: dk + # 版本 + version: 2.4.0 + # 版权年份 + copyrightYear: 2021 + # 实例演示开关 + demoEnabled: true + # 文件路径 + profile: ./dk/uploadPath + # 获取ip地址开关 + addressEnabled: true + +captcha: + # 验证码开关 + enabled: true + # 验证码类型 math 数组计算 char 字符验证 + type: math + # line 线段干扰 circle 圆圈干扰 shear 扭曲干扰 + category: circle + # 数字验证码位数 + numberLength: 1 + # 字符验证码长度 + charLength: 4 + +# 开发环境配置 +server: + # 服务器的HTTP端口,默认为8080 + port: 8082 + servlet: + # 应用的访问路径 + context-path: / + # undertow 配置 + undertow: + # HTTP post内容的最大大小。当值为-1时,默认值为大小是无限的 + max-http-post-size: -1 + # 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理 + # 每块buffer的空间大小,越小的空间被利用越充分 + buffer-size: 512 + # 是否分配的直接内存 + direct-buffers: true + threads: + # 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程 + io: 8 + # 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载 + worker: 256 +# # tomcat 配置 +# tomcat: +# # tomcat的URI编码 +# uri-encoding: UTF-8 +# # tomcat最大线程数,默认为200 +# max-threads: 500 +# # Tomcat启动初始化的线程数,默认值25 +# min-spare-threads: 30 + +# 日志配置 +logging: + level: + com.bashi: info + org.springframework: warn + config: classpath:logback.xml + +# Spring配置 +spring: + # 资源信息 + messages: + # 国际化资源文件路径 + basename: i18n/messages + profiles: + active: dev + # 文件上传 + servlet: + multipart: + # 单个文件大小 + max-file-size: 40MB + # 设置总上传的文件大小 + max-request-size: 40MB +# location: /tmp/undertow + # 服务模块 + devtools: + restart: + # 热部署开关 + enabled: true + # 与vue整合部署使用 + thymeleaf: + # 将系统模板放置到最前面 否则会与 springboot-admin 页面冲突 + template-resolver-order: 1 + jackson: + # 日期格式化 + date-format: yyyy-MM-dd HH:mm:ss + serialization: + # 格式化输出 + indent_output: false + # 忽略无法转换的对象 + fail_on_empty_beans: false + deserialization: + # 允许对象忽略json中不存在的属性 + fail_on_unknown_properties: false + +# token配置 +token: + # 令牌自定义标识 + header: Authorization + # 令牌密钥 + secret: abcdefghijklmnopqrstuvwxyz + # 令牌有效期(默认30分钟) 1一个月 30*24*60 + expireTime: 43200 + +# MyBatisPlus配置 +# https://baomidou.com/config/ +mybatis-plus: + mapperPackage: com.bashi.**.mapper + # 对应的 XML 文件位置 + mapperLocations: classpath*:mapper/**/*Mapper.xml + # 实体扫描,多个package用逗号或者分号分隔 + typeAliasesPackage: com.bashi.**.domain + # 针对 typeAliasesPackage,如果配置了该属性,则仅仅会扫描路径下以该类作为父类的域对象 + #typeAliasesSuperType: Class + # 如果配置了该属性,SqlSessionFactoryBean 会把该包下面的类注册为对应的 TypeHandler + #typeHandlersPackage: null + # 如果配置了该属性,会将路径下的枚举类进行注入,让实体类字段能够简单快捷的使用枚举属性 + #typeEnumsPackage: null + # 启动时是否检查 MyBatis XML 文件的存在,默认不检查 + checkConfigLocation: false + # 通过该属性可指定 MyBatis 的执行器,MyBatis 的执行器总共有三种: + # SIMPLE:该执行器类型不做特殊的事情,为每个语句的执行创建一个新的预处理语句(PreparedStatement) + # REUSE:该执行器类型会复用预处理语句(PreparedStatement) + # BATCH:该执行器类型会批量执行所有的更新语句 + executorType: SIMPLE + # 指定外部化 MyBatis Properties 配置,通过该配置可以抽离配置,实现不同环境的配置部署 + configurationProperties: null + configuration: + # 自动驼峰命名规则(camel case)映射 + # 如果您的数据库命名符合规则无需使用 @TableField 注解指定数据库字段名 + mapUnderscoreToCamelCase: true + # 默认枚举处理类,如果配置了该属性,枚举将统一使用指定处理器进行处理 + # org.apache.ibatis.type.EnumTypeHandler : 存储枚举的名称 + # org.apache.ibatis.type.EnumOrdinalTypeHandler : 存储枚举的索引 + # com.baomidou.mybatisplus.extension.handlers.MybatisEnumTypeHandler : 枚举类需要实现IEnum接口或字段标记@EnumValue注解. + defaultEnumTypeHandler: org.apache.ibatis.type.EnumTypeHandler + # 当设置为 true 的时候,懒加载的对象可能被任何懒属性全部加载,否则,每个属性都按需加载。需要和 lazyLoadingEnabled 一起使用。 + aggressiveLazyLoading: true + # MyBatis 自动映射策略 + # NONE:不启用自动映射 + # PARTIAL:只对非嵌套的 resultMap 进行自动映射 + # FULL:对所有的 resultMap 都进行自动映射 + autoMappingBehavior: PARTIAL + # MyBatis 自动映射时未知列或未知属性处理策 + # NONE:不做任何处理 (默认值) + # WARNING:以日志的形式打印相关警告信息 + # FAILING:当作映射失败处理,并抛出异常和详细信息 + autoMappingUnknownColumnBehavior: NONE + # Mybatis一级缓存,默认为 SESSION + # SESSION session级别缓存,同一个session相同查询语句不会再次查询数据库 + # STATEMENT 关闭一级缓存 + localCacheScope: SESSION + # 开启Mybatis二级缓存,默认为 true + cacheEnabled: true + global-config: + # 是否打印 Logo banner + banner: true + # 是否初始化 SqlRunner + enableSqlRunner: false + dbConfig: + # 主键类型 + # AUTO 数据库ID自增 + # NONE 空 + # INPUT 用户输入ID + # ASSIGN_ID 全局唯一ID + # ASSIGN_UUID 全局唯一ID UUID + idType: AUTO + # 表名前缀 + tablePrefix: null + # 字段 format,例: %s,(对主键无效) + columnFormat: null + # 表名是否使用驼峰转下划线命名,只对表名生效 + tableUnderline: true + # 大写命名,对表名和字段名均生效 + capitalMode: false + # 全局的entity的逻辑删除字段属性名 + logicDeleteField: null + # 逻辑已删除值 + logicDeleteValue: 2 + # 逻辑未删除值 + logicNotDeleteValue: 0 + # 字段验证策略之 insert,在 insert 的时候的字段验证策略 + # IGNORED 忽略判断 + # NOT_NULL 非NULL判断 + # NOT_EMPTY 非空判断(只对字符串类型字段,其他类型字段依然为非NULL判断) + # DEFAULT 默认的,一般只用于注解里 + # NEVER 不加入 SQL + insertStrategy: NOT_EMPTY + # 字段验证策略之 update,在 update 的时候的字段验证策略 + updateStrategy: NOT_EMPTY + # 字段验证策略之 select,在 select 的时候的字段验证策略既 wrapper 根据内部 entity 生成的 where 条件 + selectStrategy: NOT_EMPTY + +# Swagger配置 +swagger: + # 是否开启swagger + enabled: true + # 请求前缀 + pathMapping: /dev-api + # 标题 + title: '标题:591后台管理系统_接口文档' + # 描述 + description: '描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...' + # 版本 + version: '版本号: 2.4.0' + # 作者信息 + contact: + name: Lion Li + email: crazylionli@163.com + url: https://gitee.com/JavaLionLi/new-591 + +# 防止XSS攻击 +xss: + # 过滤开关 + enabled: true + # 排除链接(多个用逗号分隔) + excludes: /system/notice/* + # 匹配链接 + urlPatterns: /system/*,/monitor/*,/tool/* + +# 全局线程池相关配置 +thread-pool: + # 是否开启线程池 + enabled: false + # 核心线程池大小 + corePoolSize: 8 + # 最大可创建的线程数 + maxPoolSize: 16 + # 队列最大长度 + queueCapacity: 128 + # 线程池维护线程所允许的空闲时间 + keepAliveSeconds: 300 + # 线程池对拒绝任务(无线程可用)的处理策略 + # CallerRunsPolicy 等待 + # DiscardOldestPolicy 放弃最旧的 + # DiscardPolicy 丢弃 + # AbortPolicy 中止 + rejectedExecutionHandler: CallerRunsPolicy + +# feign 相关配置 +feign: + package: com.bashi.**.feign + # 开启压缩 + compression: + request: + enabled: true + response: + enabled: true + okhttp: + enabled: true + circuitbreaker: + enabled: true + +--- # 分布式锁 lock4j 全局配置 +lock4j: + # 获取分布式锁超时时间,默认为 3000 毫秒 + acquire-timeout: 3000 + # 分布式锁的超时时间,默认为 30 毫秒 + expire: 30000 + +--- # 定时任务配置 +spring: + quartz: + scheduler-name: BsScheduler + startup-delay: 1s + overwrite-existing-jobs: true + auto-startup: true + job-store-type: jdbc + properties: + org: + quartz: + # Scheduler 相关配置 + scheduler: + instanceName: BsScheduler + instanceId: AUTO + # 线程池相关配置 + threadPool: + class: org.quartz.simpl.SimpleThreadPool + threadCount: 20 + threadPriority: 5 + # JobStore 集群配置 + jobStore: + class: org.quartz.impl.jdbcjobstore.JobStoreTX + isClustered: true + clusterCheckinInterval: 15000 + txIsolationLevelSerializable: true + misfireThreshold: 60000 + tablePrefix: QRTZ_ + # sqlserver 启用 + # selectWithLockSQL: SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ? + +--- # 监控配置 +spring: + application: + name: bashi + boot: + admin: + # Spring Boot Admin Client 客户端的相关配置 + client: + # 设置 Spring Boot Admin Server 地址 + url: http://localhost:${server.port}${spring.boot.admin.context-path} + instance: + prefer-ip: true # 注册实例时,优先使用 IP + # Spring Boot Admin Server 服务端的相关配置 + context-path: /admin # 配置 Spring +dromara: + x-file-storage: + default-platform: tencent-cos-1 + aliyun-oss: + - platform: aliyun-oss-1 # 存储平台标识 + enable-storage: false # 启用存储 + access-key: 123 + secret-key: 13 + end-point: oss-cn-qingdao.aliyuncs.com + bucket-name: 123 + domain: https://file.168.com/ + base-path: dk/ + tencent-cos: + - platform: tencent-cos-1 # 存储平台标识 + enable-storage: true # 启用存储 + secret-id: IKIDy0vmjyz5AD7B80jBXnureUHxa6UgiZKs + secret-key: e4Siy8jHkEbqyuhS5b1d8VggUJ2c5PVR + region: ap-nanjing #存仓库所在地域 + bucket-name: an-download-1322700873 + domain: https://baidu.com/ # 访问域名,注意“/”结尾,例如:https://abc.cos.ap-nanjing.myqcloud.com/ + base-path: dk/ # 基础路径 + local-plus: + - platform: local-plus-1 + enable-storage: true + enable-access: true + domain: "http://127.0.0.1:8030/dk-plus/" # 访问域名,例如:“http://127.0.0.1:8030/local-plus/”,注意后面要和 path-patterns 保持一致,“/”结尾,本地存储建议使用相对路径,方便后期更换域名 + base-path: dk-plus/ # 基础路径 + path-patterns: /dk-plus/** # 访问路径 + storage-path: D:/Temp/ # 存储路径 diff --git a/bashi-admin/src/main/resources/banner.txt b/bashi-admin/src/main/resources/banner.txt new file mode 100644 index 0000000..7ad36c1 --- /dev/null +++ b/bashi-admin/src/main/resources/banner.txt @@ -0,0 +1,2 @@ +Application Version: 2.4.0 +Spring Boot Version: ${spring-boot.version} diff --git a/bashi-admin/src/main/resources/i18n/messages.properties b/bashi-admin/src/main/resources/i18n/messages.properties new file mode 100644 index 0000000..d63aa1f --- /dev/null +++ b/bashi-admin/src/main/resources/i18n/messages.properties @@ -0,0 +1,36 @@ +#错误消息 +not.null=* 必须填写 +user.jcaptcha.error=验证码错误 +user.jcaptcha.expire=验证码已失效 +user.not.exists=用户不存在/密码错误 +user.password.not.match=用户不存在/密码错误 +user.password.retry.limit.count=密码输入错误{0}次 +user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定10分钟 +user.password.delete=对不起,您的账号已被删除 +user.blocked=用户已封禁,请联系管理员 +role.blocked=角色已封禁,请联系管理员 +user.logout.success=退出成功 + +length.not.valid=长度必须在{min}到{max}个字符之间 + +user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头 +user.password.not.valid=* 5-50个字符 + +user.email.not.valid=邮箱格式错误 +user.mobile.phone.number.not.valid=手机号格式错误 +user.login.success=登录成功 +user.notfound=请重新登录 +user.forcelogout=管理员强制退出,请重新登录 +user.unknown.error=未知错误,请重新登录 + +##文件上传消息 +upload.exceed.maxSize=上传的文件大小超出限制的文件大小!
允许的文件最大大小是:{0}MB! +upload.filename.exceed.length=上传的文件名最长{0}个字符 + +##权限 +no.permission=您没有数据的权限,请联系管理员添加权限 [{0}] +no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}] +no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}] +no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}] +no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}] +no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}] diff --git a/bashi-admin/src/main/resources/logback.xml b/bashi-admin/src/main/resources/logback.xml new file mode 100644 index 0000000..8d842f0 --- /dev/null +++ b/bashi-admin/src/main/resources/logback.xml @@ -0,0 +1,71 @@ + + + + + + + + + + ${console.log.pattern} + utf-8 + + + + + + ${log.path}/sys-info.log + + + + ${log.path}/%d{yyyy-MM-dd}/sys-info.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + + + ${log.path}/sys-user.log + + + + ${log.path}/%d{yyyy-MM-dd}/sys-user.%d{yyyy-MM-dd}.log + + 20 + + + ${log.pattern} + + + + + + + + + + + + + + + + + + + + + + diff --git a/bashi-admin/src/test/java/com/bashi/BaseTest.java b/bashi-admin/src/test/java/com/bashi/BaseTest.java new file mode 100644 index 0000000..301bbd7 --- /dev/null +++ b/bashi-admin/src/test/java/com/bashi/BaseTest.java @@ -0,0 +1,10 @@ +package com.bashi; + +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class BaseTest { +} diff --git a/bashi-common/pom.xml b/bashi-common/pom.xml new file mode 100644 index 0000000..4176474 --- /dev/null +++ b/bashi-common/pom.xml @@ -0,0 +1,164 @@ + + + + bashi + com.bashi + 2.4.0 + + 4.0.0 + + bashi-common + + + common通用工具 + + + + + + + org.springframework + spring-context-support + + + + + org.springframework + spring-web + + + + + org.springframework.boot + spring-boot-starter-security + + + + + javax.validation + validation-api + + + + + org.apache.commons + commons-lang3 + + + + + com.fasterxml.jackson.core + jackson-databind + + + + + org.apache.poi + poi-ooxml + + + + + org.yaml + snakeyaml + + + + + io.jsonwebtoken + jjwt + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.apache.commons + commons-pool2 + + + + + javax.servlet + javax.servlet-api + + + + com.baomidou + mybatis-plus-boot-starter + + + com.baomidou + mybatis-plus-extension + + + cn.hutool + hutool-all + + + org.projectlombok + lombok + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + io.github.openfeign + feign-okhttp + + + + de.codecentric + spring-boot-admin-starter-server + + + de.codecentric + spring-boot-admin-starter-client + + + + com.github.xiaoymin + knife4j-spring-boot-starter + + + + + org.springframework.boot + spring-boot-configuration-processor + + + + + org.redisson + redisson-spring-boot-starter + + + + + com.baomidou + dynamic-datasource-spring-boot-starter + + + + com.baomidou + lock4j-redisson-spring-boot-starter + + + + + com.alibaba + fastjson + + + + + diff --git a/bashi-common/src/main/java/com/bashi/common/annotation/DataScope.java b/bashi-common/src/main/java/com/bashi/common/annotation/DataScope.java new file mode 100644 index 0000000..12572a4 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/annotation/DataScope.java @@ -0,0 +1,33 @@ +package com.bashi.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 数据权限过滤注解 + * + * @author duteliang + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DataScope +{ + /** + * 部门表的别名 + */ + public String deptAlias() default ""; + + /** + * 用户表的别名 + */ + public String userAlias() default ""; + + /** + * 是否过滤用户权限 + */ + public boolean isUser() default false; +} diff --git a/bashi-common/src/main/java/com/bashi/common/annotation/DataSource.java b/bashi-common/src/main/java/com/bashi/common/annotation/DataSource.java new file mode 100644 index 0000000..88b57d9 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/annotation/DataSource.java @@ -0,0 +1,28 @@ +package com.bashi.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import com.bashi.common.enums.DataSourceType; + +/** + * 自定义多数据源切换注解 + * + * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准 + * + * @author duteliang + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface DataSource +{ + /** + * 切换数据源名称 + */ + public DataSourceType value() default DataSourceType.MASTER; +} diff --git a/bashi-common/src/main/java/com/bashi/common/annotation/Excel.java b/bashi-common/src/main/java/com/bashi/common/annotation/Excel.java new file mode 100644 index 0000000..c30fd54 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/annotation/Excel.java @@ -0,0 +1,165 @@ +package com.bashi.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.math.BigDecimal; + +/** + * 自定义导出Excel数据注解 + * + * @author duteliang + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Excel +{ + /** + * 导出时在excel中排序 + */ + public int sort() default Integer.MAX_VALUE; + + /** + * 导出到Excel中的名字. + */ + public String name() default ""; + + /** + * 日期格式, 如: yyyy-MM-dd + */ + public String dateFormat() default ""; + + /** + * 如果是字典类型,请设置字典的type值 (如: sys_user_sex) + */ + public String dictType() default ""; + + /** + * 读取内容转表达式 (如: 0=男,1=女,2=未知) + */ + public String readConverterExp() default ""; + + /** + * 分隔符,读取字符串组内容 + */ + public String separator() default ","; + + /** + * BigDecimal 精度 默认:-1(默认不开启BigDecimal格式化) + */ + public int scale() default -1; + + /** + * BigDecimal 舍入规则 默认:BigDecimal.ROUND_HALF_EVEN + */ + public int roundingMode() default BigDecimal.ROUND_HALF_EVEN; + + /** + * 导出类型(0数字 1字符串) + */ + public ColumnType cellType() default ColumnType.STRING; + + /** + * 导出时在excel中每个列的高度 单位为字符 + */ + public double height() default 14; + + /** + * 导出时在excel中每个列的宽 单位为字符 + */ + public double width() default 16; + + /** + * 文字后缀,如% 90 变成90% + */ + public String suffix() default ""; + + /** + * 当值为空时,字段的默认值 + */ + public String defaultValue() default ""; + + /** + * 提示信息 + */ + public String prompt() default ""; + + /** + * 设置只能选择不能输入的列内容. + */ + public String[] combo() default {}; + + /** + * 是否导出数据,应对需求:有时我们需要导出一份模板,这是标题需要但内容需要用户手工填写. + */ + public boolean isExport() default true; + + /** + * 另一个类中的属性名称,支持多级获取,以小数点隔开 + */ + public String targetAttr() default ""; + + /** + * 是否自动统计数据,在最后追加一行统计数据总和 + */ + public boolean isStatistics() default false; + + /** + * 导出字段对齐方式(0:默认;1:靠左;2:居中;3:靠右) + */ + Align align() default Align.AUTO; + + public enum Align + { + AUTO(0), LEFT(1), CENTER(2), RIGHT(3); + private final int value; + + Align(int value) + { + this.value = value; + } + + public int value() + { + return this.value; + } + } + + /** + * 字段类型(0:导出导入;1:仅导出;2:仅导入) + */ + Type type() default Type.ALL; + + public enum Type + { + ALL(0), EXPORT(1), IMPORT(2); + private final int value; + + Type(int value) + { + this.value = value; + } + + public int value() + { + return this.value; + } + } + + public enum ColumnType + { + NUMERIC(0), STRING(1), IMAGE(2); + private final int value; + + ColumnType(int value) + { + this.value = value; + } + + public int value() + { + return this.value; + } + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/annotation/Excels.java b/bashi-common/src/main/java/com/bashi/common/annotation/Excels.java new file mode 100644 index 0000000..50ce99c --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/annotation/Excels.java @@ -0,0 +1,18 @@ +package com.bashi.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Excel注解集 + * + * @author duteliang + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Excels +{ + Excel[] value(); +} diff --git a/bashi-common/src/main/java/com/bashi/common/annotation/Log.java b/bashi-common/src/main/java/com/bashi/common/annotation/Log.java new file mode 100644 index 0000000..43f1799 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/annotation/Log.java @@ -0,0 +1,52 @@ +package com.bashi.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.bashi.common.enums.BusinessType; +import com.bashi.common.enums.OperatorType; + +/** + * 自定义操作日志记录注解 + * + * @author duteliang + */ +@Target({ElementType.PARAMETER, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Log { + /** + * 模块 + */ + String title() default ""; + + /** + * 功能 + */ + BusinessType businessType() default BusinessType.OTHER; + + /** + * 操作人类别 + */ + OperatorType operatorType() default OperatorType.MANAGE; + + boolean isSaveDb() default true; + + /** + * 是否打印数据 + */ + boolean isPrint() default true; + + /** + * 是否保存请求的参数 + */ + boolean isSaveRequestData() default true; + + /** + * 是否保存响应的参数 + */ + boolean isSaveResponseData() default true; +} diff --git a/bashi-common/src/main/java/com/bashi/common/annotation/RepeatSubmit.java b/bashi-common/src/main/java/com/bashi/common/annotation/RepeatSubmit.java new file mode 100644 index 0000000..b3a8ec4 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/annotation/RepeatSubmit.java @@ -0,0 +1,23 @@ +package com.bashi.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 自定义注解防止表单重复提交 + * + * @author duteliang + * + */ +@Inherited +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RepeatSubmit +{ + +} diff --git a/bashi-common/src/main/java/com/bashi/common/com/Condition.java b/bashi-common/src/main/java/com/bashi/common/com/Condition.java new file mode 100644 index 0000000..d01dec0 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/com/Condition.java @@ -0,0 +1,32 @@ +package com.bashi.common.com; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.metadata.OrderItem; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +/** + *

created on 2021/7/7

+ * + * @author zhangliang + */ +public class Condition { + + public Condition() { + } + + public static IPage getPage(PageParams query) { + query.check(); + Page page = new Page(query.getPageNum(), query.getPageSize()); + if(query.getOrderByColumn() != null){ + OrderItem orderItem = null; + if("desc".equals(query.getIsAsc())){ + orderItem = OrderItem.desc(query.getOrderByColumn()); + }else{ + orderItem = OrderItem.asc(query.getOrderByColumn()); + } + page.addOrder(orderItem); + } + return page; + } + +} diff --git a/bashi-common/src/main/java/com/bashi/common/com/PageParams.java b/bashi-common/src/main/java/com/bashi/common/com/PageParams.java new file mode 100644 index 0000000..a272532 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/com/PageParams.java @@ -0,0 +1,39 @@ +package com.bashi.common.com; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + *

created on 2021/7/7

+ * + * @author zhangliang + */ +@Data +@ApiModel("分页参数") +public class PageParams { + /** 分页大小 */ + @ApiModelProperty("分页大小") + private Integer pageSize; + /** 当前页数 */ + @ApiModelProperty("当前页数") + private Integer pageNum; + /** 排序列 */ + @ApiModelProperty("排序列") + @Deprecated + private String orderByColumn; + /** 排序的方向desc或者asc */ + @ApiModelProperty(value = "排序的方向", example = "asc,desc") + @Deprecated + private String isAsc; + + public void check(){ + if(pageSize == null){ + pageSize = 10; + } + if(pageNum == null){ + pageNum = 1; + } + } + +} diff --git a/bashi-common/src/main/java/com/bashi/common/config/BsConfig.java b/bashi-common/src/main/java/com/bashi/common/config/BsConfig.java new file mode 100644 index 0000000..5b8598f --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/config/BsConfig.java @@ -0,0 +1,76 @@ +package com.bashi.common.config; + +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 读取项目相关配置 + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +@Component +@ConfigurationProperties(prefix = "bashi") +public class BsConfig +{ + /** 项目名称 */ + private String name; + + /** 版本 */ + private String version; + + /** 版权年份 */ + private String copyrightYear; + + /** 实例演示开关 */ + private boolean demoEnabled; + + /** 上传路径 */ + @Getter + private static String profile; + + /** 获取地址开关 */ + @Getter + private static boolean addressEnabled; + + public void setProfile(String profile) + { + BsConfig.profile = profile; + } + + public void setAddressEnabled(boolean addressEnabled) + { + BsConfig.addressEnabled = addressEnabled; + } + + /** + * 获取头像上传路径 + */ + public static String getAvatarPath() + { + return getProfile() + "/avatar"; + } + + /** + * 获取下载路径 + */ + public static String getDownloadPath() + { + return getProfile() + "/download/"; + } + + /** + * 获取上传路径 + */ + public static String getUploadPath() + { + return getProfile() + "/upload"; + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/constant/Constants.java b/bashi-common/src/main/java/com/bashi/common/constant/Constants.java new file mode 100644 index 0000000..5707be8 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/constant/Constants.java @@ -0,0 +1,139 @@ +package com.bashi.common.constant; + +/** + * 通用常量信息 + * + * @author duteliang + */ +public class Constants +{ + /** + * UTF-8 字符集 + */ + public static final String UTF8 = "UTF-8"; + + /** + * GBK 字符集 + */ + public static final String GBK = "GBK"; + + /** + * http请求 + */ + public static final String HTTP = "http://"; + + /** + * https请求 + */ + public static final String HTTPS = "https://"; + + /** + * 通用成功标识 + */ + public static final String SUCCESS = "0"; + + /** + * 通用失败标识 + */ + public static final String FAIL = "1"; + + /** + * 登录成功 + */ + public static final String LOGIN_SUCCESS = "Success"; + + /** + * 注销 + */ + public static final String LOGOUT = "Logout"; + + /** + * 登录失败 + */ + public static final String LOGIN_FAIL = "Error"; + + /** + * 验证码 redis key + */ + public static final String CAPTCHA_CODE_KEY = "captcha_codes:"; + + /** + * 登录用户 redis key + */ + public static final String LOGIN_TOKEN_KEY = "login_tokens:"; + + /** + * 防重提交 redis key + */ + public static final String REPEAT_SUBMIT_KEY = "repeat_submit:"; + + /** + * 验证码有效期(分钟) + */ + public static final Integer CAPTCHA_EXPIRATION = 2; + + /** + * 令牌 + */ + public static final String TOKEN = "token"; + + /** + * 令牌前缀 + */ + public static final String TOKEN_PREFIX = "Bearer "; + + /** + * 令牌前缀 + */ + public static final String LOGIN_USER_KEY = "login_user_key"; + + /** + * 用户ID + */ + public static final String JWT_USERID = "userid"; + + /** + * 用户名称 + */ + public static final String JWT_USERNAME = "sub"; + + /** + * 用户头像 + */ + public static final String JWT_AVATAR = "avatar"; + + /** + * 创建时间 + */ + public static final String JWT_CREATED = "created"; + + /** + * 用户权限 + */ + public static final String JWT_AUTHORITIES = "authorities"; + + /** + * 参数管理 cache key + */ + public static final String SYS_CONFIG_KEY = "sys_config:"; + + /** + * 字典管理 cache key + */ + public static final String SYS_DICT_KEY = "sys_dict:"; + + /** + * 资源映射路径 前缀 + */ + public static final String RESOURCE_PREFIX = "/profile"; + + /** + * RMI 远程方法调用 + */ + public static final String LOOKUP_RMI = "rmi://"; + + /** + * 资源映射路径 前缀 + */ + public static final String REDIS_LOCK_KEY = "redis_lock:"; +} diff --git a/bashi-common/src/main/java/com/bashi/common/constant/DateConstant.java b/bashi-common/src/main/java/com/bashi/common/constant/DateConstant.java new file mode 100644 index 0000000..4538054 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/constant/DateConstant.java @@ -0,0 +1,13 @@ +package com.bashi.common.constant; + +/** + *

created on 2021/7/15

+ * + * @author zhangliang + */ +public class DateConstant { + + public final static String PATTERN_DATETIME = "yyyy-MM-dd HH:mm:ss"; + + public final static String PATTERN_DATE = "yyyy-MM-dd"; +} diff --git a/bashi-common/src/main/java/com/bashi/common/constant/GenConstants.java b/bashi-common/src/main/java/com/bashi/common/constant/GenConstants.java new file mode 100644 index 0000000..5078c70 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/constant/GenConstants.java @@ -0,0 +1,114 @@ +package com.bashi.common.constant; + +/** + * 代码生成通用常量 + * + * @author duteliang + */ +public class GenConstants +{ + /** 单表(增删改查) */ + public static final String TPL_CRUD = "crud"; + + /** 树表(增删改查) */ + public static final String TPL_TREE = "tree"; + + /** 主子表(增删改查) */ + public static final String TPL_SUB = "sub"; + + /** 树编码字段 */ + public static final String TREE_CODE = "treeCode"; + + /** 树父编码字段 */ + public static final String TREE_PARENT_CODE = "treeParentCode"; + + /** 树名称字段 */ + public static final String TREE_NAME = "treeName"; + + /** 上级菜单ID字段 */ + public static final String PARENT_MENU_ID = "parentMenuId"; + + /** 上级菜单名称字段 */ + public static final String PARENT_MENU_NAME = "parentMenuName"; + + /** 数据库字符串类型 */ + public static final String[] COLUMNTYPE_STR = { "char", "varchar", "nvarchar", "varchar2" }; + + /** 数据库文本类型 */ + public static final String[] COLUMNTYPE_TEXT = { "tinytext", "text", "mediumtext", "longtext" }; + + /** 数据库时间类型 */ + public static final String[] COLUMNTYPE_TIME = { "datetime", "time", "date", "timestamp" }; + + /** 数据库数字类型 */ + public static final String[] COLUMNTYPE_NUMBER = { "tinyint", "smallint", "mediumint", "int", "number", "integer", + "bit", "bigint", "float", "double", "decimal" }; + + /** 页面不需要编辑字段 */ + public static final String[] COLUMNNAME_NOT_EDIT = { "id", "create_by", "create_time", "del_flag" }; + + /** 页面不需要显示的列表字段 */ + public static final String[] COLUMNNAME_NOT_LIST = { "id", "create_by", "create_time", "del_flag", "update_by", + "update_time" }; + + /** 页面不需要查询字段 */ + public static final String[] COLUMNNAME_NOT_QUERY = { "id", "create_by", "create_time", "del_flag", "update_by", + "update_time", "remark" }; + + /** Entity基类字段 */ + public static final String[] BASE_ENTITY = { "createBy", "createTime", "updateBy", "updateTime", "remark" }; + + /** Tree基类字段 */ + public static final String[] TREE_ENTITY = { "parentName", "parentId", "orderNum", "ancestors", "children" }; + + /** 文本框 */ + public static final String HTML_INPUT = "input"; + + /** 文本域 */ + public static final String HTML_TEXTAREA = "textarea"; + + /** 下拉框 */ + public static final String HTML_SELECT = "select"; + + /** 单选框 */ + public static final String HTML_RADIO = "radio"; + + /** 复选框 */ + public static final String HTML_CHECKBOX = "checkbox"; + + /** 日期控件 */ + public static final String HTML_DATETIME = "datetime"; + + /** 图片上传控件 */ + public static final String HTML_IMAGE_UPLOAD = "imageUpload"; + + /** 文件上传控件 */ + public static final String HTML_FILE_UPLOAD = "fileUpload"; + + /** 富文本控件 */ + public static final String HTML_EDITOR = "editor"; + + /** 字符串类型 */ + public static final String TYPE_STRING = "String"; + + /** 整型 */ + public static final String TYPE_INTEGER = "Integer"; + + /** 长整型 */ + public static final String TYPE_LONG = "Long"; + + /** 浮点型 */ + public static final String TYPE_DOUBLE = "Double"; + + /** 高精度计算类型 */ + public static final String TYPE_BIGDECIMAL = "BigDecimal"; + + /** 时间类型 */ + public static final String TYPE_DATE = "Date"; + + /** 模糊查询 */ + public static final String QUERY_LIKE = "LIKE"; + + /** 需要 */ + public static final String REQUIRE = "1"; +} diff --git a/bashi-common/src/main/java/com/bashi/common/constant/ScheduleConstants.java b/bashi-common/src/main/java/com/bashi/common/constant/ScheduleConstants.java new file mode 100644 index 0000000..e9c7bd2 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/constant/ScheduleConstants.java @@ -0,0 +1,50 @@ +package com.bashi.common.constant; + +/** + * 任务调度通用常量 + * + * @author duteliang + */ +public class ScheduleConstants +{ + public static final String TASK_CLASS_NAME = "TASK_CLASS_NAME"; + + /** 执行目标key */ + public static final String TASK_PROPERTIES = "TASK_PROPERTIES"; + + /** 默认 */ + public static final String MISFIRE_DEFAULT = "0"; + + /** 立即触发执行 */ + public static final String MISFIRE_IGNORE_MISFIRES = "1"; + + /** 触发一次执行 */ + public static final String MISFIRE_FIRE_AND_PROCEED = "2"; + + /** 不触发立即执行 */ + public static final String MISFIRE_DO_NOTHING = "3"; + + public enum Status + { + /** + * 正常 + */ + NORMAL("0"), + /** + * 暂停 + */ + PAUSE("1"); + + private String value; + + private Status(String value) + { + this.value = value; + } + + public String getValue() + { + return value; + } + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/constant/UserConstants.java b/bashi-common/src/main/java/com/bashi/common/constant/UserConstants.java new file mode 100644 index 0000000..42ac007 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/constant/UserConstants.java @@ -0,0 +1,63 @@ +package com.bashi.common.constant; + +/** + * 用户常量信息 + * + * @author duteliang + */ +public class UserConstants +{ + /** + * 平台内系统用户的唯一标志 + */ + public static final String SYS_USER = "SYS_USER"; + + /** 正常状态 */ + public static final String NORMAL = "0"; + + /** 异常状态 */ + public static final String EXCEPTION = "1"; + + /** 用户封禁状态 */ + public static final String USER_DISABLE = "1"; + + /** 角色封禁状态 */ + public static final String ROLE_DISABLE = "1"; + + /** 部门正常状态 */ + public static final String DEPT_NORMAL = "0"; + + /** 部门停用状态 */ + public static final String DEPT_DISABLE = "1"; + + /** 字典正常状态 */ + public static final String DICT_NORMAL = "0"; + + /** 是否为系统默认(是) */ + public static final String YES = "Y"; + + /** 是否菜单外链(是) */ + public static final String YES_FRAME = "0"; + + /** 是否菜单外链(否) */ + public static final String NO_FRAME = "1"; + + /** 菜单类型(目录) */ + public static final String TYPE_DIR = "M"; + + /** 菜单类型(菜单) */ + public static final String TYPE_MENU = "C"; + + /** 菜单类型(按钮) */ + public static final String TYPE_BUTTON = "F"; + + /** Layout组件标识 */ + public final static String LAYOUT = "Layout"; + + /** ParentView组件标识 */ + public final static String PARENT_VIEW = "ParentView"; + + /** 校验返回结果码 */ + public final static String UNIQUE = "0"; + public final static String NOT_UNIQUE = "1"; +} diff --git a/bashi-common/src/main/java/com/bashi/common/core/controller/BaseController.java b/bashi-common/src/main/java/com/bashi/common/core/controller/BaseController.java new file mode 100644 index 0000000..234b60a --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/core/controller/BaseController.java @@ -0,0 +1,70 @@ +package com.bashi.common.core.controller; + +import cn.hutool.core.util.StrUtil; +import com.bashi.common.core.domain.AjaxResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * web层通用数据处理 + * + * @author duteliang + */ +public class BaseController { + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + + /** + * 响应返回结果 + * + * @param rows 影响行数 + * @return 操作结果 + */ + protected AjaxResult toAjax(int rows) { + return rows > 0 ? AjaxResult.success() : AjaxResult.error(); + } + + /** + * 响应返回结果 + * + * @param result 结果 + * @return 操作结果 + */ + protected AjaxResult toAjax(boolean result) { + return result ? success() : error(); + } + + /** + * 返回成功 + */ + public AjaxResult success() { + return AjaxResult.success(); + } + + /** + * 返回失败消息 + */ + public AjaxResult error() { + return AjaxResult.error(); + } + + /** + * 返回成功消息 + */ + public AjaxResult success(String message) { + return AjaxResult.success(message); + } + + /** + * 返回失败消息 + */ + public AjaxResult error(String message) { + return AjaxResult.error(message); + } + + /** + * 页面跳转 + */ + public String redirect(String url) { + return StrUtil.format("redirect:{}", url); + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/core/domain/AjaxResult.java b/bashi-common/src/main/java/com/bashi/common/core/domain/AjaxResult.java new file mode 100644 index 0000000..f99e36d --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/core/domain/AjaxResult.java @@ -0,0 +1,134 @@ +package com.bashi.common.core.domain; + +import cn.hutool.http.HttpStatus; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * 操作消息提醒 + * + * @author Lion Li + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@ApiModel("请求响应对象") +public class AjaxResult { + + private static final long serialVersionUID = 1L; + + /** + * 状态码 + */ + @ApiModelProperty("消息状态码") + private int code; + + /** + * 返回内容 + */ + @ApiModelProperty("消息内容") + private String msg; + + /** + * 数据对象 + */ + @ApiModelProperty("数据对象") + private T data; + + /** + * 初始化一个新创建的 AjaxResult 对象 + * + * @param code 状态码 + * @param msg 返回内容 + */ + public AjaxResult(int code, String msg) { + this.code = code; + this.msg = msg; + } + + /** + * 返回成功消息 + * + * @return 成功消息 + */ + public static AjaxResult success() { + return AjaxResult.success("操作成功"); + } + + /** + * 返回成功数据 + * + * @return 成功消息 + */ + public static AjaxResult success(T data) { + return AjaxResult.success("操作成功", data); + } + + /** + * 返回成功消息 + * + * @param msg 返回内容 + * @return 成功消息 + */ + public static AjaxResult success(String msg) { + return AjaxResult.success(msg, null); + } + + /** + * 返回成功消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 成功消息 + */ + public static AjaxResult success(String msg, T data) { + return new AjaxResult<>(HttpStatus.HTTP_OK, msg, data); + } + + /** + * 返回错误消息 + * + * @return + */ + public static AjaxResult error() { + return AjaxResult.error("操作失败"); + } + + /** + * 返回错误消息 + * + * @param msg 返回内容 + * @return 警告消息 + */ + public static AjaxResult error(String msg) { + return AjaxResult.error(msg, null); + } + + /** + * 返回错误消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 警告消息 + */ + public static AjaxResult error(String msg, T data) { + return new AjaxResult<>(HttpStatus.HTTP_BAD_REQUEST, msg, data); + } + + /** + * 返回错误消息 + * + * @param code 状态码 + * @param msg 返回内容 + * @return 警告消息 + */ + public static AjaxResult error(int code, String msg) { + return new AjaxResult<>(code, msg, null); + } + +} diff --git a/bashi-common/src/main/java/com/bashi/common/core/domain/BaseEntity.java b/bashi-common/src/main/java/com/bashi/common/core/domain/BaseEntity.java new file mode 100644 index 0000000..be66f50 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/core/domain/BaseEntity.java @@ -0,0 +1,46 @@ +package com.bashi.common.core.domain; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * Entity基类 + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class BaseEntity implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 搜索值 */ + private String searchValue; + + /** 创建者 */ + private String createBy; + + /** 创建时间 */ + private Date createTime; + + /** 更新者 */ + private String updateBy; + + /** 更新时间 */ + private Date updateTime; + + /** 备注 */ + private String remark; + + /** 请求参数 */ + private Map params = new HashMap<>(); + +} diff --git a/bashi-common/src/main/java/com/bashi/common/core/domain/TreeEntity.java b/bashi-common/src/main/java/com/bashi/common/core/domain/TreeEntity.java new file mode 100644 index 0000000..d6db02d --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/core/domain/TreeEntity.java @@ -0,0 +1,38 @@ +package com.bashi.common.core.domain; + +import lombok.*; +import lombok.experimental.Accessors; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tree基类 + * + * @author duteliang + */ + +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class TreeEntity extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 父菜单名称 */ + private String parentName; + + /** 父菜单ID */ + private Long parentId; + + /** 显示顺序 */ + private Integer orderNum; + + /** 祖级列表 */ + private String ancestors; + + /** 子部门 */ + private List children = new ArrayList<>(); + +} diff --git a/bashi-common/src/main/java/com/bashi/common/core/domain/TreeSelect.java b/bashi-common/src/main/java/com/bashi/common/core/domain/TreeSelect.java new file mode 100644 index 0000000..64ea2e0 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/core/domain/TreeSelect.java @@ -0,0 +1,50 @@ +package com.bashi.common.core.domain; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.bashi.common.core.domain.entity.SysDept; +import com.bashi.common.core.domain.entity.SysMenu; +import lombok.*; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Treeselect树结构实体类 + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class TreeSelect implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 节点ID */ + private Long id; + + /** 节点名称 */ + private String label; + + /** 子节点 */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private List children; + + public TreeSelect(SysDept dept) + { + this.id = dept.getDeptId(); + this.label = dept.getDeptName(); + this.children = dept.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + public TreeSelect(SysMenu menu) + { + this.id = menu.getMenuId(); + this.label = menu.getMenuName(); + this.children = menu.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList()); + } + +} diff --git a/bashi-common/src/main/java/com/bashi/common/core/domain/entity/Customer.java b/bashi-common/src/main/java/com/bashi/common/core/domain/entity/Customer.java new file mode 100644 index 0000000..d64ba8b --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/core/domain/entity/Customer.java @@ -0,0 +1,115 @@ +package com.bashi.common.core.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.bashi.common.constant.DateConstant; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 客户 + * @TableName dk_customer + */ +@TableName(value ="dk_customer") +@Data +@ApiModel("客户模型") +public class Customer implements Serializable { + /** + * ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 手机 + */ + @ApiModelProperty("手机") + private String phoneNumber; + + /** + * 用户名称 + */ + @ApiModelProperty("用户名称") + private String nickName; + + /** + * 用户密码 + */ + @ApiModelProperty("用户密码") + private String password; + + /** + * 是否实名 + */ + @ApiModelProperty("是否实名") + private Integer realNameAuth; + + /** + * 是否贷款 + */ + @ApiModelProperty("是否贷款") + private Integer loansFlag; + + @ApiModelProperty("允许提现") + private Boolean allowWithdrawFlag; + + /** + * 是否提现 + */ + @ApiModelProperty("是否提现") + private Integer withdrawFlag; + + /** + * 余额 + */ + @ApiModelProperty("余额") + private BigDecimal account; + + @ApiModelProperty("贷款金额") + private BigDecimal borrowAccount; + + @ApiModelProperty("还款金额") + private BigDecimal repaymentAccount; + + /** + * 状态 0-正常 1-封禁 + */ + @ApiModelProperty("状态 0-正常 1-封禁") + private Integer status; + + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonFormat(pattern = DateConstant.PATTERN_DATETIME) + @ApiModelProperty("最后登陆时间") + private LocalDateTime lastLoginTime; + + @ApiModelProperty("最后登陆IP") + private String lastLoginIp; + + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonFormat(pattern = DateConstant.PATTERN_DATETIME) + @ApiModelProperty("修改时间") + private LocalDateTime updateTime; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; + + @JsonIgnore + public String getPassword() { + return password; + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysDept.java b/bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysDept.java new file mode 100644 index 0000000..851562b --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysDept.java @@ -0,0 +1,99 @@ +package com.bashi.common.core.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.io.Serializable; +import java.util.*; + +/** + * 部门表 sys_dept + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +@TableName("sys_dept") +public class SysDept implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 部门ID */ + @TableId(value = "dept_id",type = IdType.AUTO) + private Long deptId; + + /** 父部门ID */ + private Long parentId; + + /** 祖级列表 */ + private String ancestors; + + /** 部门名称 */ + @NotBlank(message = "部门名称不能为空") + @Size(min = 0, max = 30, message = "部门名称长度不能超过30个字符") + private String deptName; + + /** 显示顺序 */ + @NotBlank(message = "显示顺序不能为空") + private String orderNum; + + /** 负责人 */ + private String leader; + + /** 联系电话 */ + @Size(min = 0, max = 11, message = "联系电话长度不能超过11个字符") + private String phone; + + /** 邮箱 */ + @Email(message = "邮箱格式不正确") + @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符") + private String email; + + /** 部门状态:0正常,1停用 */ + private String status; + + /** 删除标志(0代表存在 2代表删除) */ + @TableLogic + private String delFlag; + + /** 父部门名称 */ + @TableField(exist = false) + private String parentName; + + /** 创建者 */ + @TableField(fill = FieldFill.INSERT) + private String createBy; + + /** 创建时间 */ + @TableField(fill = FieldFill.INSERT) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** 更新者 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateBy; + + /** 更新时间 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** 子部门 */ + @TableField(exist = false) + private List children = new ArrayList(); + + /** + * 请求参数 + */ + @TableField(exist = false) + private Map params = new HashMap<>(); + +} diff --git a/bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysDictData.java b/bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysDictData.java new file mode 100644 index 0000000..35d212f --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysDictData.java @@ -0,0 +1,107 @@ +package com.bashi.common.core.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.bashi.common.annotation.Excel; +import com.bashi.common.annotation.Excel.ColumnType; +import com.bashi.common.constant.UserConstants; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * 字典数据表 sys_dict_data + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +@TableName("sys_dict_data") +public class SysDictData implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 字典编码 */ + @Excel(name = "字典编码", cellType = ColumnType.NUMERIC) + @TableId(value = "dict_code",type = IdType.AUTO) + private Long dictCode; + + /** 字典排序 */ + @Excel(name = "字典排序", cellType = ColumnType.NUMERIC) + private Long dictSort; + + /** 字典标签 */ + @Excel(name = "字典标签") + @NotBlank(message = "字典标签不能为空") + @Size(min = 0, max = 100, message = "字典标签长度不能超过100个字符") + private String dictLabel; + + /** 字典键值 */ + @Excel(name = "字典键值") + @NotBlank(message = "字典键值不能为空") + @Size(min = 0, max = 100, message = "字典键值长度不能超过100个字符") + private String dictValue; + + /** 字典类型 */ + @Excel(name = "字典类型") + @NotBlank(message = "字典类型不能为空") + @Size(min = 0, max = 100, message = "字典类型长度不能超过100个字符") + private String dictType; + + /** 样式属性(其他样式扩展) */ + @Size(min = 0, max = 100, message = "样式属性长度不能超过100个字符") + private String cssClass; + + /** 表格字典样式 */ + private String listClass; + + /** 是否默认(Y是 N否) */ + @Excel(name = "是否默认", readConverterExp = "Y=是,N=否") + private String isDefault; + + /** 状态(0正常 1停用) */ + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** 创建者 */ + @TableField(fill = FieldFill.INSERT) + private String createBy; + + /** 创建时间 */ + @TableField(fill = FieldFill.INSERT) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** 更新者 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateBy; + + /** 更新时间 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** 备注 */ + private String remark; + + /** + * 请求参数 + */ + @TableField(exist = false) + private Map params = new HashMap<>(); + + public boolean getDefault() + { + return UserConstants.YES.equals(this.isDefault) ? true : false; + } + +} diff --git a/bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysDictType.java b/bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysDictType.java new file mode 100644 index 0000000..0885da9 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysDictType.java @@ -0,0 +1,80 @@ +package com.bashi.common.core.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.bashi.common.annotation.Excel; +import com.bashi.common.annotation.Excel.ColumnType; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * 字典类型表 sys_dict_type + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +@TableName("sys_dict_type") +public class SysDictType implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 字典主键 */ + @Excel(name = "字典主键", cellType = ColumnType.NUMERIC) + @TableId(value = "dict_id",type = IdType.AUTO) + private Long dictId; + + /** 字典名称 */ + @Excel(name = "字典名称") + @NotBlank(message = "字典名称不能为空") + @Size(min = 0, max = 100, message = "字典类型名称长度不能超过100个字符") + private String dictName; + + /** 字典类型 */ + @Excel(name = "字典类型") + @NotBlank(message = "字典类型不能为空") + @Size(min = 0, max = 100, message = "字典类型类型长度不能超过100个字符") + private String dictType; + + /** 状态(0正常 1停用) */ + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** 创建者 */ + @TableField(fill = FieldFill.INSERT) + private String createBy; + + /** 创建时间 */ + @TableField(fill = FieldFill.INSERT) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** 更新者 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateBy; + + /** 更新时间 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** 备注 */ + private String remark; + + /** + * 请求参数 + */ + @TableField(exist = false) + private Map params = new HashMap<>(); + +} diff --git a/bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysMenu.java b/bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysMenu.java new file mode 100644 index 0000000..04ca021 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysMenu.java @@ -0,0 +1,108 @@ +package com.bashi.common.core.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.io.Serializable; +import java.util.*; + +/** + * 菜单权限表 sys_menu + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +@TableName("sys_menu") +public class SysMenu implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 菜单ID */ + @TableId(value = "menu_id",type = IdType.AUTO) + private Long menuId; + + /** 菜单名称 */ + @NotBlank(message = "菜单名称不能为空") + @Size(min = 0, max = 50, message = "菜单名称长度不能超过50个字符") + private String menuName; + + /** 父菜单名称 */ + @TableField(exist = false) + private String parentName; + + /** 父菜单ID */ + private Long parentId; + + /** 显示顺序 */ + @NotBlank(message = "显示顺序不能为空") + private String orderNum; + + /** 路由地址 */ + @Size(min = 0, max = 200, message = "路由地址不能超过200个字符") + private String path; + + /** 组件路径 */ + @Size(min = 0, max = 200, message = "组件路径不能超过255个字符") + private String component; + + /** 是否为外链(0是 1否) */ + private String isFrame; + + /** 是否缓存(0缓存 1不缓存) */ + private String isCache; + + /** 类型(M目录 C菜单 F按钮) */ + @NotBlank(message = "菜单类型不能为空") + private String menuType; + + /** 显示状态(0显示 1隐藏) */ + private String visible; + + /** 菜单状态(0显示 1隐藏) */ + private String status; + + /** 权限字符串 */ + @Size(min = 0, max = 100, message = "权限标识长度不能超过100个字符") + private String perms; + + /** 菜单图标 */ + private String icon; + + /** 创建者 */ + @TableField(fill = FieldFill.INSERT) + private String createBy; + + /** 创建时间 */ + @TableField(fill = FieldFill.INSERT) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** 更新者 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateBy; + + /** 更新时间 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** 备注 */ + private String remark; + + /** 请求参数 */ + @TableField(exist = false) + private Map params = new HashMap<>(); + + /** 子菜单 */ + @TableField(exist = false) + private List children = new ArrayList(); + +} diff --git a/bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysRole.java b/bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysRole.java new file mode 100644 index 0000000..a1e0f52 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysRole.java @@ -0,0 +1,126 @@ +package com.bashi.common.core.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.bashi.common.annotation.Excel; +import com.bashi.common.annotation.Excel.ColumnType; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * 角色表 sys_role + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +@TableName("sys_role") +public class SysRole implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 角色ID */ + @Excel(name = "角色序号", cellType = ColumnType.NUMERIC) + @TableId(value = "role_id",type = IdType.AUTO) + private Long roleId; + + /** 角色名称 */ + @Excel(name = "角色名称") + @NotBlank(message = "角色名称不能为空") + @Size(min = 0, max = 30, message = "角色名称长度不能超过30个字符") + private String roleName; + + /** 角色权限 */ + @Excel(name = "角色权限") + @NotBlank(message = "权限字符不能为空") + @Size(min = 0, max = 100, message = "权限字符长度不能超过100个字符") + private String roleKey; + + /** 角色排序 */ + @Excel(name = "角色排序") + @NotBlank(message = "显示顺序不能为空") + private String roleSort; + + /** 数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限) */ + @Excel(name = "数据范围", readConverterExp = "1=所有数据权限,2=自定义数据权限,3=本部门数据权限,4=本部门及以下数据权限,5=仅本人数据权限") + private String dataScope; + + /** 菜单树选择项是否关联显示( 0:父子不互相关联显示 1:父子互相关联显示) */ + private boolean menuCheckStrictly; + + /** 部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示 ) */ + private boolean deptCheckStrictly; + + /** 角色状态(0正常 1停用) */ + @Excel(name = "角色状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** 删除标志(0代表存在 2代表删除) */ + @TableLogic + private String delFlag; + + /** 创建者 */ + @TableField(fill = FieldFill.INSERT) + private String createBy; + + /** 创建时间 */ + @TableField(fill = FieldFill.INSERT) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** 更新者 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateBy; + + /** 更新时间 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** 备注 */ + private String remark; + + /** + * 请求参数 + */ + @TableField(exist = false) + private Map params = new HashMap<>(); + + /** 用户是否存在此角色标识 默认不存在 */ + @TableField(exist = false) + private boolean flag = false; + + /** 菜单组 */ + @TableField(exist = false) + private Long[] menuIds; + + /** 部门组(数据权限) */ + @TableField(exist = false) + private Long[] deptIds; + + public SysRole(Long roleId) + { + this.roleId = roleId; + } + + public boolean isAdmin() + { + return isAdmin(this.roleId); + } + + public static boolean isAdmin(Long roleId) + { + return roleId != null && 1L == roleId; + } + +} diff --git a/bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysUser.java b/bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysUser.java new file mode 100644 index 0000000..3de13a3 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/core/domain/entity/SysUser.java @@ -0,0 +1,168 @@ +package com.bashi.common.core.domain.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.bashi.common.annotation.Excel; +import com.bashi.common.annotation.Excel.ColumnType; +import com.bashi.common.annotation.Excel.Type; +import com.bashi.common.annotation.Excels; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 用户对象 sys_user + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +@TableName("sys_user") +public class SysUser implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 用户ID */ + @Excel(name = "用户序号", cellType = ColumnType.NUMERIC, prompt = "用户编号") + @TableId(value = "user_id",type = IdType.AUTO) + private Long userId; + + /** 部门ID */ + @Excel(name = "部门编号", type = Type.IMPORT) + private Long deptId; + + /** 用户账号 */ + @NotBlank(message = "用户账号不能为空") + @Size(min = 0, max = 30, message = "用户账号长度不能超过30个字符") + @Excel(name = "登录名称") + private String userName; + + /** 用户昵称 */ + @Size(min = 0, max = 30, message = "用户昵称长度不能超过30个字符") + @Excel(name = "用户名称") + private String nickName; + + /** 用户邮箱 */ + @Email(message = "邮箱格式不正确") + @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符") + @Excel(name = "用户邮箱") + private String email; + + /** 手机号码 */ + @Size(min = 0, max = 11, message = "手机号码长度不能超过11个字符") + @Excel(name = "手机号码") + private String phonenumber; + + /** 用户性别 */ + @Excel(name = "用户性别", readConverterExp = "0=男,1=女,2=未知") + private String sex; + + /** 用户头像 */ + private String avatar; + + private String openId; + + /** 密码 */ + private String password; + + @JsonIgnore + @JsonProperty + public String getPassword() { + return password; + } + + /** 盐加密 */ + private String salt; + + /** 帐号状态(0正常 1停用) */ + @Excel(name = "帐号状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** 删除标志(0代表存在 2代表删除) */ + @TableLogic + private String delFlag; + + /** 最后登录IP */ + @Excel(name = "最后登录IP", type = Type.EXPORT) + private String loginIp; + + /** 最后登录时间 */ + @Excel(name = "最后登录时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss", type = Type.EXPORT) + private Date loginDate; + + /** 创建者 */ + @TableField(fill = FieldFill.INSERT) + private String createBy; + + /** 创建时间 */ + @TableField(fill = FieldFill.INSERT) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** 更新者 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateBy; + + /** 更新时间 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** 备注 */ + private String remark; + + /** + * 请求参数 + */ + @TableField(exist = false) + private Map params = new HashMap<>(); + + /** 部门对象 */ + @Excels({ + @Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT), + @Excel(name = "部门负责人", targetAttr = "leader", type = Type.EXPORT) + }) + @TableField(exist = false) + private SysDept dept; + + /** 角色对象 */ + @TableField(exist = false) + private List roles; + + /** 角色组 */ + @TableField(exist = false) + private Long[] roleIds; + + /** 岗位组 */ + @TableField(exist = false) + private Long[] postIds; + + public SysUser(Long userId) + { + this.userId = userId; + } + + public boolean isAdmin() + { + return isAdmin(this.userId); + } + + public static boolean isAdmin(Long userId) + { + return userId != null && 1L == userId; + } + +} diff --git a/bashi-common/src/main/java/com/bashi/common/core/domain/model/LoginBody.java b/bashi-common/src/main/java/com/bashi/common/core/domain/model/LoginBody.java new file mode 100644 index 0000000..e394a12 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/core/domain/model/LoginBody.java @@ -0,0 +1,37 @@ +package com.bashi.common.core.domain.model; + +import lombok.*; +import lombok.experimental.Accessors; + +/** + * 用户登录对象 + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class LoginBody +{ + /** + * 用户名 + */ + private String username; + + /** + * 用户密码 + */ + private String password; + + /** + * 验证码 + */ + private String code; + + /** + * 唯一标识 + */ + private String uuid = ""; + +} diff --git a/bashi-common/src/main/java/com/bashi/common/core/domain/model/LoginPhoneBody.java b/bashi-common/src/main/java/com/bashi/common/core/domain/model/LoginPhoneBody.java new file mode 100644 index 0000000..17275f1 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/core/domain/model/LoginPhoneBody.java @@ -0,0 +1,39 @@ +package com.bashi.common.core.domain.model; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + *

created on 2021/7/13

+ * + * @author zhangliang + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +@ApiModel("登录模型") +public class LoginPhoneBody implements Serializable { + + private static final long serialVersionUID=1L; + + @ApiModelProperty("公众号登录必填") + private String openId; + + @ApiModelProperty("手机号") + private String mobile; + + @ApiModelProperty("密码") + private String password; + + @ApiModelProperty("验证码") + private String mobileCode; + + @ApiModelProperty("用户-1") + private Integer loginRole; + +} diff --git a/bashi-common/src/main/java/com/bashi/common/core/domain/model/LoginUser.java b/bashi-common/src/main/java/com/bashi/common/core/domain/model/LoginUser.java new file mode 100644 index 0000000..ae3489f --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/core/domain/model/LoginUser.java @@ -0,0 +1,142 @@ +package com.bashi.common.core.domain.model; + +import com.bashi.common.core.domain.entity.Customer; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.bashi.common.core.domain.entity.SysUser; +import lombok.*; +import lombok.experimental.Accessors; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Set; + +/** + * 登录用户身份权限 + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class LoginUser implements UserDetails { + private static final long serialVersionUID = 1L; + + /** + * 用户唯一标识 + */ + private String token; + + /** + * 登录时间 + */ + private Long loginTime; + + /** + * 过期时间 + */ + private Long expireTime; + + /** + * 登录IP地址 + */ + private String ipaddr; + + /** + * 登录地点 + */ + private String loginLocation; + + /** + * 浏览器类型 + */ + private String browser; + + /** + * 操作系统 + */ + private String os; + + /** + * 权限列表 + */ + private Set permissions; + + /** + * 用户信息 + */ + private SysUser user; + + private Customer customer; + /** + * 0-user + * 1-sms + */ + private Integer type = 0; + + public LoginUser(SysUser user, Set permissions) { + this.user = user; + this.permissions = permissions; + } + + @JsonIgnore + @Override + public String getPassword() { + return user.getPassword(); + } + + @JsonIgnore + @Override + public String getUsername() { + return type == 1 ? customer.getPhoneNumber() : user.getUserName(); + } + + /** + * 账户是否未过期,过期无法验证 + */ + @JsonIgnore + @Override + public boolean isAccountNonExpired() { + return true; + } + + /** + * 指定用户是否解锁,锁定的用户无法进行身份验证 + * + * @return + */ + @JsonIgnore + @Override + public boolean isAccountNonLocked() { + return true; + } + + /** + * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证 + * + * @return + */ + @JsonIgnore + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + /** + * 是否可用 ,禁用的用户不能身份验证 + * + * @return + */ + @JsonIgnore + @Override + public boolean isEnabled() { + return true; + } + + @JsonIgnore + @Override + public Collection getAuthorities() { + return null; + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/core/mybatisplus/cache/MybatisPlusRedisCache.java b/bashi-common/src/main/java/com/bashi/common/core/mybatisplus/cache/MybatisPlusRedisCache.java new file mode 100644 index 0000000..4abd5c5 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/core/mybatisplus/cache/MybatisPlusRedisCache.java @@ -0,0 +1,102 @@ +package com.bashi.common.core.mybatisplus.cache; + +import cn.hutool.extra.spring.SpringUtil; +import com.bashi.common.core.redis.RedisCache; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.cache.Cache; +import org.springframework.data.redis.connection.RedisServerCommands; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.util.CollectionUtils; + +import java.util.Collection; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * mybatis-redis 二级缓存 + * + * @author Lion Li + */ +@Slf4j +public class MybatisPlusRedisCache implements Cache { + + private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true); + + private RedisCache redisCache; + + private String id; + + public MybatisPlusRedisCache(final String id) { + if (id == null) { + throw new IllegalArgumentException("Cache instances require an ID"); + } + this.id = id; + } + + @Override + public String getId() { + return this.id; + } + + @Override + public void putObject(Object key, Object value) { + if (redisCache == null) { + redisCache = SpringUtil.getBean(RedisCache.class); + } + if (value != null) { + redisCache.setCacheObject(key.toString(), value); + } + } + + @Override + public Object getObject(Object key) { + if (redisCache == null) { + //由于启动期间注入失败,只能运行期间注入,这段代码可以删除 + redisCache = SpringUtil.getBean(RedisCache.class); + } + try { + if (key != null) { + return redisCache.getCacheObject(key.toString()); + } + } catch (Exception e) { + e.printStackTrace(); + log.error("缓存出错"); + } + return null; + } + + @Override + public Object removeObject(Object key) { + if (redisCache == null) { + redisCache = SpringUtil.getBean(RedisCache.class); + } + if (key != null) { + redisCache.deleteObject(key.toString()); + } + return null; + } + + @Override + public void clear() { + log.debug("清空缓存"); + if (redisCache == null) { + redisCache = SpringUtil.getBean(RedisCache.class); + } + Collection keys = redisCache.keys("*:" + this.id + "*"); + if (!CollectionUtils.isEmpty(keys)) { + redisCache.deleteObject(keys); + } + } + + @Override + public int getSize() { + RedisTemplate redisTemplate = SpringUtil.getBean("redisTemplate"); + Long size = redisTemplate.execute(RedisServerCommands::dbSize); + return size.intValue(); + } + + @Override + public ReadWriteLock getReadWriteLock() { + return this.readWriteLock; + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/core/mybatisplus/core/BaseMapperPlus.java b/bashi-common/src/main/java/com/bashi/common/core/mybatisplus/core/BaseMapperPlus.java new file mode 100644 index 0000000..27de8e3 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/core/mybatisplus/core/BaseMapperPlus.java @@ -0,0 +1,18 @@ +package com.bashi.common.core.mybatisplus.core; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; + +/** + * 自定义 Mapper 接口, 实现 自定义扩展 + * + * @author Lion Li + * @since 2021-05-13 + */ +public interface BaseMapperPlus extends BaseMapper { + + int insertAll(@Param("list") Collection batchList); + +} diff --git a/bashi-common/src/main/java/com/bashi/common/core/mybatisplus/methods/InsertAll.java b/bashi-common/src/main/java/com/bashi/common/core/mybatisplus/methods/InsertAll.java new file mode 100644 index 0000000..1975ec9 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/core/mybatisplus/methods/InsertAll.java @@ -0,0 +1,52 @@ +package com.bashi.common.core.mybatisplus.methods; + +import cn.hutool.core.util.StrUtil; +import org.apache.ibatis.executor.keygen.NoKeyGenerator; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlSource; + +import com.baomidou.mybatisplus.core.injector.AbstractMethod; +import com.baomidou.mybatisplus.core.metadata.TableInfo; + +/** + * 单sql批量插入 + * + * @author Lion Li + */ +public class InsertAll extends AbstractMethod { + + @Override + public MappedStatement injectMappedStatement(Class mapperClass, Class modelClass, TableInfo tableInfo) { + final String sql = ""; + final String fieldSql = prepareFieldSql(tableInfo); + final String valueSql = prepareValuesSqlForMysqlBatch(tableInfo); + final String sqlResult = String.format(sql, tableInfo.getTableName(), fieldSql, valueSql); + SqlSource sqlSource = languageDriver.createSqlSource(configuration, sqlResult, modelClass); + return this.addInsertMappedStatement(mapperClass, modelClass, "insertAll", sqlSource, new NoKeyGenerator(), null, null); + } + + private String prepareFieldSql(TableInfo tableInfo) { + StringBuilder fieldSql = new StringBuilder(); + if (StrUtil.isNotBlank(tableInfo.getKeyColumn())) { + fieldSql.append(tableInfo.getKeyColumn()).append(","); + } + tableInfo.getFieldList().forEach(x -> fieldSql.append(x.getColumn()).append(",")); + fieldSql.delete(fieldSql.length() - 1, fieldSql.length()); + fieldSql.insert(0, "("); + fieldSql.append(")"); + return fieldSql.toString(); + } + + private String prepareValuesSqlForMysqlBatch(TableInfo tableInfo) { + final StringBuilder valueSql = new StringBuilder(); + valueSql.append(""); + if (StrUtil.isNotBlank(tableInfo.getKeyColumn())) { + valueSql.append("#{item.").append(tableInfo.getKeyProperty()).append("},"); + } + tableInfo.getFieldList().forEach(x -> valueSql.append("#{item.").append(x.getProperty()).append("},")); + valueSql.delete(valueSql.length() - 1, valueSql.length()); + valueSql.append(""); + return valueSql.toString(); + } +} + diff --git a/bashi-common/src/main/java/com/bashi/common/core/page/PagePlus.java b/bashi-common/src/main/java/com/bashi/common/core/page/PagePlus.java new file mode 100644 index 0000000..ec0f6cd --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/core/page/PagePlus.java @@ -0,0 +1,156 @@ +package com.bashi.common.core.page; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.metadata.OrderItem; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * 分页 Page 增强对象 + * + * @param 数据库实体 + * @param vo实体 + * @author Lion Li + */ +@Data +@Accessors(chain = true) +public class PagePlus implements IPage { + + /** + * domain实体列表 + */ + private List records = Collections.emptyList(); + + /** + * vo实体列表 + */ + private List recordsVo = Collections.emptyList(); + + /** + * 总数 + */ + private long total = 0L; + + /** + * 页长度 + */ + private long size = 10L; + + /** + * 当前页 + */ + private long current = 1L; + + /** + * 排序字段信息 + */ + private List orders = new ArrayList<>(); + + /** + * 自动优化 COUNT SQL + */ + private boolean optimizeCountSql = true; + + /** + * 是否进行 count 查询 + */ + private boolean isSearchCount = true; + + /** + * 是否命中count缓存 + */ + private boolean hitCount = false; + + /** + * countId + */ + private String countId; + + /** + * 最大limit + */ + private Long maxLimit; + + public PagePlus() { + } + + public PagePlus(long current, long size) { + this(current, size, 0L); + } + + public PagePlus(long current, long size, long total) { + this(current, size, total, true); + } + + public PagePlus(long current, long size, boolean isSearchCount) { + this(current, size, 0L, isSearchCount); + } + + public PagePlus(long current, long size, long total, boolean isSearchCount) { + if (current > 1L) { + this.current = current; + } + this.size = size; + this.total = total; + this.isSearchCount = isSearchCount; + } + + @Override + public String countId() { + return this.getCountId(); + } + + @Override + public Long maxLimit() { + return this.getMaxLimit(); + } + + public PagePlus addOrder(OrderItem... items) { + this.orders.addAll(Arrays.asList(items)); + return this; + } + + public PagePlus addOrder(List items) { + this.orders.addAll(items); + return this; + } + + @Override + public List orders() { + return this.getOrders(); + } + + @Override + public boolean optimizeCountSql() { + return this.optimizeCountSql; + } + + @Override + public long getPages() { + // 解决 github issues/3208 + return IPage.super.getPages(); + } + + public static PagePlus of(long current, long size) { + return of(current, size, 0); + } + + public static PagePlus of(long current, long size, long total) { + return of(current, size, total, true); + } + + public static PagePlus of(long current, long size, boolean searchCount) { + return of(current, size, 0, searchCount); + } + + public static PagePlus of(long current, long size, long total, boolean searchCount) { + return new PagePlus<>(current, size, total, searchCount); + } + +} + diff --git a/bashi-common/src/main/java/com/bashi/common/core/page/TableDataInfo.java b/bashi-common/src/main/java/com/bashi/common/core/page/TableDataInfo.java new file mode 100644 index 0000000..4c380d7 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/core/page/TableDataInfo.java @@ -0,0 +1,60 @@ +package com.bashi.common.core.page; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.List; + +/** + * 表格分页数据对象 + * + * @author Lion Li + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +@ApiModel("分页响应对象") +public class TableDataInfo implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 总记录数 + */ + @ApiModelProperty("总记录数") + private long total; + + /** + * 列表数据 + */ + @ApiModelProperty("列表数据") + private List rows; + + /** + * 消息状态码 + */ + @ApiModelProperty("消息状态码") + private int code; + + /** + * 消息内容 + */ + @ApiModelProperty("消息内容") + private String msg; + + /** + * 分页 + * + * @param list 列表数据 + * @param total 总记录数 + */ + public TableDataInfo(List list, long total) { + this.rows = list; + this.total = total; + } + +} diff --git a/bashi-common/src/main/java/com/bashi/common/core/redis/RedisCache.java b/bashi-common/src/main/java/com/bashi/common/core/redis/RedisCache.java new file mode 100644 index 0000000..cea542c --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/core/redis/RedisCache.java @@ -0,0 +1,223 @@ +package com.bashi.common.core.redis; + +import com.google.common.collect.Lists; +import org.redisson.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * spring redis 工具类 + * + * @author shenxinquan + **/ +@SuppressWarnings(value = {"unchecked", "rawtypes"}) +@Component +public class RedisCache { + + @Autowired + private RedissonClient redissonClient; + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + */ + public void setCacheObject(final String key, final T value) { + redissonClient.getBucket(key).set(value); + } + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + * @param timeout 时间 + * @param timeUnit 时间颗粒度 + */ + public void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) { + RBucket result = redissonClient.getBucket(key); + result.set(value); + result.expire(timeout, timeUnit); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @return true=设置成功;false=设置失败 + */ + public boolean expire(final String key, final long timeout) { + return expire(key, timeout, TimeUnit.SECONDS); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @param unit 时间单位 + * @return true=设置成功;false=设置失败 + */ + public boolean expire(final String key, final long timeout, final TimeUnit unit) { + RBucket rBucket = redissonClient.getBucket(key); + return rBucket.expire(timeout, unit); + } + + /** + * 获得缓存的基本对象。 + * + * @param key 缓存键值 + * @return 缓存键值对应的数据 + */ + public T getCacheObject(final String key) { + RBucket rBucket = redissonClient.getBucket(key); + return rBucket.get(); + } + + /** + * 删除单个对象 + * + * @param key + */ + public boolean deleteObject(final String key) { + return redissonClient.getBucket(key).delete(); + } + + /* */ + + /** + * 删除集合对象 + * + * @param collection 多个对象 + * @return + */ + public void deleteObject(final Collection collection) { + RBatch batch = redissonClient.createBatch(); + collection.forEach(t->{ + batch.getBucket(t.toString()).deleteAsync(); + }); + batch.execute(); + } + + /** + * 缓存List数据 + * + * @param key 缓存的键值 + * @param dataList 待缓存的List数据 + * @return 缓存的对象 + */ + public boolean setCacheList(final String key, final List dataList) { + RList rList = redissonClient.getList(key); + return rList.addAll(dataList); + } + + /** + * 获得缓存的list对象 + * + * @param key 缓存的键值 + * @return 缓存键值对应的数据 + */ + public List getCacheList(final String key) { + RList rList = redissonClient.getList(key); + return rList.readAll(); + } + + /** + * 缓存Set + * + * @param key 缓存键值 + * @param dataSet 缓存的数据 + * @return 缓存数据的对象 + */ + public boolean setCacheSet(final String key, final Set dataSet) { + RSet rSet = redissonClient.getSet(key); + return rSet.addAll(dataSet); + } + + /** + * 获得缓存的set + * + * @param key + * @return + */ + public Set getCacheSet(final String key) { + RSet rSet = redissonClient.getSet(key); + return rSet.readAll(); + } + + /** + * 缓存Map + * + * @param key + * @param dataMap + */ + public void setCacheMap(final String key, final Map dataMap) { + if (dataMap != null) { + RMap rMap = redissonClient.getMap(key); + rMap.putAll(dataMap); + } + } + + /** + * 获得缓存的Map + * + * @param key + * @return + */ + public Map getCacheMap(final String key) { + RMap rMap = redissonClient.getMap(key); + return rMap.getAll(rMap.keySet()); + } + + /** + * 往Hash中存入数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @param value 值 + */ + public void setCacheMapValue(final String key, final String hKey, final T value) { + RMap rMap = redissonClient.getMap(key); + rMap.put(hKey, value); + } + + /** + * 获取Hash中的数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @return Hash中的对象 + */ + public T getCacheMapValue(final String key, final String hKey) { + RMap rMap = redissonClient.getMap(key); + return rMap.get(hKey); + } + + /** + * 获取多个Hash中的数据 + * + * @param key Redis键 + * @param hKeys Hash键集合 + * @return Hash对象集合 + */ + public List getMultiCacheMapValue(final String key, final Collection hKeys) { + RListMultimap rListMultimap = redissonClient.getListMultimap(key); + return rListMultimap.getAll(hKeys); + } + + /** + * 获得缓存的基本对象列表 + * + * @param pattern 字符串前缀 + * @return 对象列表 + */ + public Collection keys(final String pattern) { + Iterable iterable = redissonClient.getKeys().getKeysByPattern(pattern); + return Lists.newArrayList(iterable); + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/enums/BusinessStatus.java b/bashi-common/src/main/java/com/bashi/common/enums/BusinessStatus.java new file mode 100644 index 0000000..e4817e5 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/enums/BusinessStatus.java @@ -0,0 +1,20 @@ +package com.bashi.common.enums; + +/** + * 操作状态 + * + * @author duteliang + * + */ +public enum BusinessStatus +{ + /** + * 成功 + */ + SUCCESS, + + /** + * 失败 + */ + FAIL, +} diff --git a/bashi-common/src/main/java/com/bashi/common/enums/BusinessType.java b/bashi-common/src/main/java/com/bashi/common/enums/BusinessType.java new file mode 100644 index 0000000..c5086ff --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/enums/BusinessType.java @@ -0,0 +1,59 @@ +package com.bashi.common.enums; + +/** + * 业务操作类型 + * + * @author duteliang + */ +public enum BusinessType +{ + /** + * 其它 + */ + OTHER, + + /** + * 新增 + */ + INSERT, + + /** + * 修改 + */ + UPDATE, + + /** + * 删除 + */ + DELETE, + + /** + * 授权 + */ + GRANT, + + /** + * 导出 + */ + EXPORT, + + /** + * 导入 + */ + IMPORT, + + /** + * 强退 + */ + FORCE, + + /** + * 生成代码 + */ + GENCODE, + + /** + * 清空数据 + */ + CLEAN, +} diff --git a/bashi-common/src/main/java/com/bashi/common/enums/DataSourceType.java b/bashi-common/src/main/java/com/bashi/common/enums/DataSourceType.java new file mode 100644 index 0000000..bcac247 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/enums/DataSourceType.java @@ -0,0 +1,25 @@ +package com.bashi.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 数据源 + * + * @author Lion Li + */ +@AllArgsConstructor +public enum DataSourceType { + /** + * 主库 + */ + MASTER("master"), + + /** + * 从库 + */ + SLAVE("slave"); + + @Getter + private final String source; +} diff --git a/bashi-common/src/main/java/com/bashi/common/enums/HttpMethod.java b/bashi-common/src/main/java/com/bashi/common/enums/HttpMethod.java new file mode 100644 index 0000000..04738f4 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/enums/HttpMethod.java @@ -0,0 +1,36 @@ +package com.bashi.common.enums; + +import java.util.HashMap; +import java.util.Map; +import org.springframework.lang.Nullable; + +/** + * 请求方式 + * + * @author duteliang + */ +public enum HttpMethod +{ + GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE; + + private static final Map mappings = new HashMap<>(16); + + static + { + for (HttpMethod httpMethod : values()) + { + mappings.put(httpMethod.name(), httpMethod); + } + } + + @Nullable + public static HttpMethod resolve(@Nullable String method) + { + return (method != null ? mappings.get(method) : null); + } + + public boolean matches(String method) + { + return (this == resolve(method)); + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/enums/OperatorType.java b/bashi-common/src/main/java/com/bashi/common/enums/OperatorType.java new file mode 100644 index 0000000..c9b2436 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/enums/OperatorType.java @@ -0,0 +1,24 @@ +package com.bashi.common.enums; + +/** + * 操作人类别 + * + * @author duteliang + */ +public enum OperatorType +{ + /** + * 其它 + */ + OTHER, + + /** + * 后台用户 + */ + MANAGE, + + /** + * 手机端用户 + */ + MOBILE +} diff --git a/bashi-common/src/main/java/com/bashi/common/enums/UserStatus.java b/bashi-common/src/main/java/com/bashi/common/enums/UserStatus.java new file mode 100644 index 0000000..c9839d2 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/enums/UserStatus.java @@ -0,0 +1,30 @@ +package com.bashi.common.enums; + +/** + * 用户状态 + * + * @author duteliang + */ +public enum UserStatus +{ + OK("0", "正常"), DISABLE("1", "停用"), DELETED("2", "删除"); + + private final String code; + private final String info; + + UserStatus(String code, String info) + { + this.code = code; + this.info = info; + } + + public String getCode() + { + return code; + } + + public String getInfo() + { + return info; + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/exception/BaseException.java b/bashi-common/src/main/java/com/bashi/common/exception/BaseException.java new file mode 100644 index 0000000..1c12498 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/exception/BaseException.java @@ -0,0 +1,97 @@ +package com.bashi.common.exception; + +import cn.hutool.core.lang.Validator; +import com.bashi.common.utils.MessageUtils; + +/** + * 基础异常 + * + * @author duteliang + */ +public class BaseException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + /** + * 所属模块 + */ + private String module; + + /** + * 错误码 + */ + private String code; + + /** + * 错误码对应的参数 + */ + private Object[] args; + + /** + * 错误消息 + */ + private String defaultMessage; + + public BaseException(String module, String code, Object[] args, String defaultMessage) + { + this.module = module; + this.code = code; + this.args = args; + this.defaultMessage = defaultMessage; + } + + public BaseException(String module, String code, Object[] args) + { + this(module, code, args, null); + } + + public BaseException(String module, String defaultMessage) + { + this(module, null, null, defaultMessage); + } + + public BaseException(String code, Object[] args) + { + this(null, code, args, null); + } + + public BaseException(String defaultMessage) + { + this(null, null, null, defaultMessage); + } + + @Override + public String getMessage() + { + String message = null; + if (!Validator.isEmpty(code)) + { + message = MessageUtils.message(code, args); + } + if (message == null) + { + message = defaultMessage; + } + return message; + } + + public String getModule() + { + return module; + } + + public String getCode() + { + return code; + } + + public Object[] getArgs() + { + return args; + } + + public String getDefaultMessage() + { + return defaultMessage; + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/exception/CustomException.java b/bashi-common/src/main/java/com/bashi/common/exception/CustomException.java new file mode 100644 index 0000000..db71731 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/exception/CustomException.java @@ -0,0 +1,45 @@ +package com.bashi.common.exception; + +import cn.hutool.http.HttpStatus; + +/** + * 自定义异常 + * + * @author duteliang + */ +public class CustomException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + private Integer code = HttpStatus.HTTP_BAD_REQUEST; + + private String message; + + public CustomException(String message) + { + this.message = message; + } + + public CustomException(String message, Integer code) + { + this.message = message; + this.code = code; + } + + public CustomException(String message, Throwable e) + { + super(message, e); + this.message = message; + } + + @Override + public String getMessage() + { + return message; + } + + public Integer getCode() + { + return code; + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/exception/DemoModeException.java b/bashi-common/src/main/java/com/bashi/common/exception/DemoModeException.java new file mode 100644 index 0000000..a533977 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/exception/DemoModeException.java @@ -0,0 +1,15 @@ +package com.bashi.common.exception; + +/** + * 演示模式异常 + * + * @author duteliang + */ +public class DemoModeException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public DemoModeException() + { + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/exception/UtilException.java b/bashi-common/src/main/java/com/bashi/common/exception/UtilException.java new file mode 100644 index 0000000..4814fa1 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/exception/UtilException.java @@ -0,0 +1,26 @@ +package com.bashi.common.exception; + +/** + * 工具类异常 + * + * @author duteliang + */ +public class UtilException extends RuntimeException +{ + private static final long serialVersionUID = 8247610319171014183L; + + public UtilException(Throwable e) + { + super(e.getMessage(), e); + } + + public UtilException(String message) + { + super(message); + } + + public UtilException(String message, Throwable throwable) + { + super(message, throwable); + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/exception/file/FileException.java b/bashi-common/src/main/java/com/bashi/common/exception/file/FileException.java new file mode 100644 index 0000000..479c8ef --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/exception/file/FileException.java @@ -0,0 +1,19 @@ +package com.bashi.common.exception.file; + +import com.bashi.common.exception.BaseException; + +/** + * 文件信息异常类 + * + * @author duteliang + */ +public class FileException extends BaseException +{ + private static final long serialVersionUID = 1L; + + public FileException(String code, Object[] args) + { + super("file", code, args, null); + } + +} diff --git a/bashi-common/src/main/java/com/bashi/common/exception/file/FileNameLengthLimitExceededException.java b/bashi-common/src/main/java/com/bashi/common/exception/file/FileNameLengthLimitExceededException.java new file mode 100644 index 0000000..2958da1 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/exception/file/FileNameLengthLimitExceededException.java @@ -0,0 +1,16 @@ +package com.bashi.common.exception.file; + +/** + * 文件名称超长限制异常类 + * + * @author duteliang + */ +public class FileNameLengthLimitExceededException extends FileException +{ + private static final long serialVersionUID = 1L; + + public FileNameLengthLimitExceededException(int defaultFileNameLength) + { + super("upload.filename.exceed.length", new Object[] { defaultFileNameLength }); + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/exception/file/FileSizeLimitExceededException.java b/bashi-common/src/main/java/com/bashi/common/exception/file/FileSizeLimitExceededException.java new file mode 100644 index 0000000..2e22798 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/exception/file/FileSizeLimitExceededException.java @@ -0,0 +1,16 @@ +package com.bashi.common.exception.file; + +/** + * 文件名大小限制异常类 + * + * @author duteliang + */ +public class FileSizeLimitExceededException extends FileException +{ + private static final long serialVersionUID = 1L; + + public FileSizeLimitExceededException(long defaultMaxSize) + { + super("upload.exceed.maxSize", new Object[] { defaultMaxSize }); + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/exception/file/InvalidExtensionException.java b/bashi-common/src/main/java/com/bashi/common/exception/file/InvalidExtensionException.java new file mode 100644 index 0000000..1867e4c --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/exception/file/InvalidExtensionException.java @@ -0,0 +1,81 @@ +package com.bashi.common.exception.file; + +import java.util.Arrays; +import org.apache.commons.fileupload.FileUploadException; + +/** + * 文件上传 误异常类 + * + * @author duteliang + */ +public class InvalidExtensionException extends FileUploadException +{ + private static final long serialVersionUID = 1L; + + private String[] allowedExtension; + private String extension; + private String filename; + + public InvalidExtensionException(String[] allowedExtension, String extension, String filename) + { + super("filename : [" + filename + "], extension : [" + extension + "], allowed extension : [" + Arrays.toString(allowedExtension) + "]"); + this.allowedExtension = allowedExtension; + this.extension = extension; + this.filename = filename; + } + + public String[] getAllowedExtension() + { + return allowedExtension; + } + + public String getExtension() + { + return extension; + } + + public String getFilename() + { + return filename; + } + + public static class InvalidImageExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidImageExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidFlashExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidFlashExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidMediaExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidMediaExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidVideoExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidVideoExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/exception/job/TaskException.java b/bashi-common/src/main/java/com/bashi/common/exception/job/TaskException.java new file mode 100644 index 0000000..2718f06 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/exception/job/TaskException.java @@ -0,0 +1,34 @@ +package com.bashi.common.exception.job; + +/** + * 计划策略异常 + * + * @author duteliang + */ +public class TaskException extends Exception +{ + private static final long serialVersionUID = 1L; + + private Code code; + + public TaskException(String msg, Code code) + { + this(msg, code, null); + } + + public TaskException(String msg, Code code, Exception nestedEx) + { + super(msg, nestedEx); + this.code = code; + } + + public Code getCode() + { + return code; + } + + public enum Code + { + TASK_EXISTS, NO_TASK_EXISTS, TASK_ALREADY_STARTED, UNKNOWN, CONFIG_ERROR, TASK_NODE_NOT_AVAILABLE + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/exception/user/CaptchaException.java b/bashi-common/src/main/java/com/bashi/common/exception/user/CaptchaException.java new file mode 100644 index 0000000..d5fe5ba --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/exception/user/CaptchaException.java @@ -0,0 +1,16 @@ +package com.bashi.common.exception.user; + +/** + * 验证码错误异常类 + * + * @author duteliang + */ +public class CaptchaException extends UserException +{ + private static final long serialVersionUID = 1L; + + public CaptchaException() + { + super("user.jcaptcha.error", null); + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/exception/user/CaptchaExpireException.java b/bashi-common/src/main/java/com/bashi/common/exception/user/CaptchaExpireException.java new file mode 100644 index 0000000..eb81054 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/exception/user/CaptchaExpireException.java @@ -0,0 +1,16 @@ +package com.bashi.common.exception.user; + +/** + * 验证码失效异常类 + * + * @author duteliang + */ +public class CaptchaExpireException extends UserException +{ + private static final long serialVersionUID = 1L; + + public CaptchaExpireException() + { + super("user.jcaptcha.expire", null); + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/exception/user/UserException.java b/bashi-common/src/main/java/com/bashi/common/exception/user/UserException.java new file mode 100644 index 0000000..94ba5f7 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/exception/user/UserException.java @@ -0,0 +1,18 @@ +package com.bashi.common.exception.user; + +import com.bashi.common.exception.BaseException; + +/** + * 用户信息异常类 + * + * @author duteliang + */ +public class UserException extends BaseException +{ + private static final long serialVersionUID = 1L; + + public UserException(String code, Object[] args) + { + super("user", code, args, null); + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/exception/user/UserPasswordNotMatchException.java b/bashi-common/src/main/java/com/bashi/common/exception/user/UserPasswordNotMatchException.java new file mode 100644 index 0000000..040134d --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/exception/user/UserPasswordNotMatchException.java @@ -0,0 +1,16 @@ +package com.bashi.common.exception.user; + +/** + * 用户密码不正确或不符合规范异常类 + * + * @author duteliang + */ +public class UserPasswordNotMatchException extends UserException +{ + private static final long serialVersionUID = 1L; + + public UserPasswordNotMatchException() + { + super("user.password.not.match", null); + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/filter/RepeatableFilter.java b/bashi-common/src/main/java/com/bashi/common/filter/RepeatableFilter.java new file mode 100644 index 0000000..4bad342 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/filter/RepeatableFilter.java @@ -0,0 +1,48 @@ +package com.bashi.common.filter; + +import cn.hutool.core.util.StrUtil; +import org.springframework.http.MediaType; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * Repeatable 过滤器 + * + * @author duteliang + */ +public class RepeatableFilter implements Filter +{ + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException + { + ServletRequest requestWrapper = null; + if (request instanceof HttpServletRequest + && StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) + { + requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response); + } + if (null == requestWrapper) + { + chain.doFilter(request, response); + } + else + { + chain.doFilter(requestWrapper, response); + } + } + + @Override + public void destroy() + { + + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/filter/RepeatedlyRequestWrapper.java b/bashi-common/src/main/java/com/bashi/common/filter/RepeatedlyRequestWrapper.java new file mode 100644 index 0000000..495a3f1 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/filter/RepeatedlyRequestWrapper.java @@ -0,0 +1,77 @@ +package com.bashi.common.filter; + +import cn.hutool.core.io.IoUtil; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +/** + * 构建可重复读取inputStream的request + * + * @author duteliang + */ +public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper +{ + private final byte[] body; + + public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException + { + super(request); + request.setCharacterEncoding("UTF-8"); + response.setCharacterEncoding("UTF-8"); + + body = IoUtil.readUtf8(request.getInputStream()).getBytes(StandardCharsets.UTF_8); + } + + @Override + public BufferedReader getReader() throws IOException + { + return new BufferedReader(new InputStreamReader(getInputStream())); + } + + @Override + public ServletInputStream getInputStream() throws IOException + { + final ByteArrayInputStream bais = new ByteArrayInputStream(body); + return new ServletInputStream() + { + @Override + public int read() throws IOException + { + return bais.read(); + } + + @Override + public int available() throws IOException + { + return body.length; + } + + @Override + public boolean isFinished() + { + return false; + } + + @Override + public boolean isReady() + { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) + { + + } + }; + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/filter/XssFilter.java b/bashi-common/src/main/java/com/bashi/common/filter/XssFilter.java new file mode 100644 index 0000000..d612f98 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/filter/XssFilter.java @@ -0,0 +1,93 @@ +package com.bashi.common.filter; + +import cn.hutool.core.util.StrUtil; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 防止XSS攻击的过滤器 + * + * @author duteliang + */ +public class XssFilter implements Filter +{ + /** + * 排除链接 + */ + public List excludes = new ArrayList<>(); + + /** + * xss过滤开关 + */ + public boolean enabled = false; + + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + String tempExcludes = filterConfig.getInitParameter("excludes"); + String tempEnabled = filterConfig.getInitParameter("enabled"); + if (StrUtil.isNotEmpty(tempExcludes)) + { + String[] url = tempExcludes.split(","); + for (int i = 0; url != null && i < url.length; i++) + { + excludes.add(url[i]); + } + } + if (StrUtil.isNotEmpty(tempEnabled)) + { + enabled = Boolean.valueOf(tempEnabled); + } + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException + { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + if (handleExcludeURL(req, resp)) + { + chain.doFilter(request, response); + return; + } + XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request); + chain.doFilter(xssRequest, response); + } + + private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response) + { + if (!enabled) + { + return true; + } + if (excludes == null || excludes.isEmpty()) + { + return false; + } + String url = request.getServletPath(); + for (String pattern : excludes) + { + Pattern p = Pattern.compile("^" + pattern); + Matcher m = p.matcher(url); + if (m.find()) + { + return true; + } + } + return false; + } + + @Override + public void destroy() + { + + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/filter/XssHttpServletRequestWrapper.java b/bashi-common/src/main/java/com/bashi/common/filter/XssHttpServletRequestWrapper.java new file mode 100644 index 0000000..53d9253 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/filter/XssHttpServletRequestWrapper.java @@ -0,0 +1,108 @@ +package com.bashi.common.filter; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HtmlUtil; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * XSS过滤处理 + * + * @author duteliang + */ +public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper +{ + /** + * @param request + */ + public XssHttpServletRequestWrapper(HttpServletRequest request) + { + super(request); + } + + @Override + public String[] getParameterValues(String name) + { + String[] values = super.getParameterValues(name); + if (values != null) + { + int length = values.length; + String[] escapseValues = new String[length]; + for (int i = 0; i < length; i++) + { + // 防xss攻击和过滤前后空格 + escapseValues[i] = HtmlUtil.cleanHtmlTag(values[i]).trim(); + } + return escapseValues; + } + return super.getParameterValues(name); + } + + @Override + public ServletInputStream getInputStream() throws IOException + { + // 非json类型,直接返回 + if (!isJsonRequest()) + { + return super.getInputStream(); + } + + // 为空,直接返回 + String json = IoUtil.read(super.getInputStream(), StandardCharsets.UTF_8); + if (Validator.isEmpty(json)) + { + return super.getInputStream(); + } + + // xss过滤 + json = HtmlUtil.cleanHtmlTag(json).trim(); + + final ByteArrayInputStream bis = IoUtil.toStream(json, StandardCharsets.UTF_8); + return new ServletInputStream() + { + @Override + public boolean isFinished() + { + return true; + } + + @Override + public boolean isReady() + { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) + { + } + + @Override + public int read() throws IOException + { + return bis.read(); + } + }; + } + + /** + * 是否是Json请求 + * + * @param request + */ + public boolean isJsonRequest() + { + String header = super.getHeader(HttpHeaders.CONTENT_TYPE); + return StrUtil.startWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE); + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/utils/BeanContextUtils.java b/bashi-common/src/main/java/com/bashi/common/utils/BeanContextUtils.java new file mode 100644 index 0000000..034d2f6 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/utils/BeanContextUtils.java @@ -0,0 +1,29 @@ +package com.bashi.common.utils; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * spring bean工具类 + */ +@Component +public class BeanContextUtils implements ApplicationContextAware { + + private static ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext app) { + if (applicationContext == null) { + applicationContext = app; + } + } + + public static T getBean(Class tClass) { + return applicationContext.getBean(tClass); + } + + public static Object getBean(String className) { + return applicationContext.getBean(className); + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/utils/BeanConvertUtil.java b/bashi-common/src/main/java/com/bashi/common/utils/BeanConvertUtil.java new file mode 100644 index 0000000..1869a76 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/utils/BeanConvertUtil.java @@ -0,0 +1,102 @@ +package com.bashi.common.utils; + +import com.google.common.collect.Lists; +import org.springframework.beans.BeanUtils; + +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +/** + * bean工具 + * @Author: 004795 + * @Date: 2022/5/24 11:11 + */ +public class BeanConvertUtil extends BeanUtils { + + private BeanConvertUtil(){} + + @FunctionalInterface + public interface CallBack { + /** + * 回调方法 + * + * @param s 源对象 + * @param t 目标对象 + */ + void callBack(S s, T t); + } + + /** + * 转换list 对象 + * + * @param sources 源对象 + * @param targetSupplier 目标对象供应方 + * @param callBack 回调方法 + * @param 源对象类型 + * @param 目标对象类型 + * @Return 转换对象 + */ + public static List convertListTo(List sources, Supplier targetSupplier, CallBack callBack) { + if (null == sources || null == targetSupplier) { + return Collections.emptyList(); + } + List list = Lists.newArrayListWithCapacity(sources.size()); + for (S source : sources) { + T target = targetSupplier.get(); + copyProperties(source, target); + if (callBack != null) { + callBack.callBack(source, target); + } + list.add(target); + } + return list; + } + + /** + * 转换list 对象 + * + * @param sources 源对象 + * @param targetSupplier 目标对象供应方 + * @param 源对象类型 + * @param 目标对象类型 + */ + public static List convertListTo(List sources, Supplier targetSupplier) { + return convertListTo(sources, targetSupplier, null); + } + + /** + * 转换对象 + * + * @param source 源对象 + * @param targetSupplier 目标对象供应方 + * @param 源对象类型 + * @param 目标对象类型 + * @return 目标对象 + */ + public static T convertTo(S source, Supplier targetSupplier) { + return convertTo(source, targetSupplier, null); + } + + /** + * 转换对象 + * + * @param source 源对象 + * @param targetSupplier 目标对象供应方 + * @param callBack 回调方法 + * @param 源对象类型 + * @param 目标对象类型 + * @return 目标对象 + */ + public static T convertTo(S source, Supplier targetSupplier, CallBack callBack) { + if (null == source || null == targetSupplier) { + return null; + } + T target = targetSupplier.get(); + copyProperties(source, target); + if (callBack != null) { + callBack.callBack(source, target); + } + return target; + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/utils/DateUtils.java b/bashi-common/src/main/java/com/bashi/common/utils/DateUtils.java new file mode 100644 index 0000000..d203417 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/utils/DateUtils.java @@ -0,0 +1,155 @@ +package com.bashi.common.utils; + +import java.lang.management.ManagementFactory; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import org.apache.commons.lang3.time.DateFormatUtils; + +/** + * 时间工具类 + * + * @author duteliang + */ +public class DateUtils extends org.apache.commons.lang3.time.DateUtils +{ + public static String YYYY = "yyyy"; + + public static String YYYY_MM = "yyyy-MM"; + + public static String YYYY_MM_DD = "yyyy-MM-dd"; + + public static String YYYYMMDDHHMMSS = "yyyyMMddHHmmss"; + + public static String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss"; + + private static String[] parsePatterns = { + "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM", + "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM", + "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"}; + + /** + * 获取当前Date型日期 + * + * @return Date() 当前日期 + */ + public static Date getNowDate() + { + return new Date(); + } + + /** + * 获取当前日期, 默认格式为yyyy-MM-dd + * + * @return String + */ + public static String getDate() + { + return dateTimeNow(YYYY_MM_DD); + } + + public static final String getTime() + { + return dateTimeNow(YYYY_MM_DD_HH_MM_SS); + } + + public static final String dateTimeNow() + { + return dateTimeNow(YYYYMMDDHHMMSS); + } + + public static final String dateTimeNow(final String format) + { + return parseDateToStr(format, new Date()); + } + + public static final String dateTime(final Date date) + { + return parseDateToStr(YYYY_MM_DD, date); + } + + public static final String parseDateToStr(final String format, final Date date) + { + return new SimpleDateFormat(format).format(date); + } + + public static final Date dateTime(final String format, final String ts) + { + try + { + return new SimpleDateFormat(format).parse(ts); + } + catch (ParseException e) + { + throw new RuntimeException(e); + } + } + + /** + * 日期路径 即年/月/日 如2018/08/08 + */ + public static final String datePath() + { + Date now = new Date(); + return DateFormatUtils.format(now, "yyyy/MM/dd"); + } + + /** + * 日期路径 即年/月/日 如20180808 + */ + public static final String dateTime() + { + Date now = new Date(); + return DateFormatUtils.format(now, "yyyyMMdd"); + } + + /** + * 日期型字符串转化为日期 格式 + */ + public static Date parseDate(Object str) + { + if (str == null) + { + return null; + } + try + { + return parseDate(str.toString(), parsePatterns); + } + catch (ParseException e) + { + return null; + } + } + + /** + * 获取服务器启动时间 + */ + public static Date getServerStartDate() + { + long time = ManagementFactory.getRuntimeMXBean().getStartTime(); + return new Date(time); + } + + /** + * 计算两个时间差 + */ + public static String getDatePoor(Date endDate, Date nowDate) + { + long nd = 1000 * 24 * 60 * 60; + long nh = 1000 * 60 * 60; + long nm = 1000 * 60; + // long ns = 1000; + // 获得两个时间的毫秒时间差异 + long diff = endDate.getTime() - nowDate.getTime(); + // 计算差多少天 + long day = diff / nd; + // 计算差多少小时 + long hour = diff % nd / nh; + // 计算差多少分钟 + long min = diff % nd % nh / nm; + // 计算差多少秒//输出结果 + // long sec = diff % nd % nh % nm / ns; + return day + "天" + hour + "小时" + min + "分钟"; + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/utils/DictUtils.java b/bashi-common/src/main/java/com/bashi/common/utils/DictUtils.java new file mode 100644 index 0000000..acfac19 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/utils/DictUtils.java @@ -0,0 +1,187 @@ +package com.bashi.common.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.StrUtil; +import com.bashi.common.constant.Constants; +import com.bashi.common.core.domain.entity.SysDictData; +import com.bashi.common.core.redis.RedisCache; +import com.bashi.common.utils.spring.SpringUtils; + +import java.util.Collection; +import java.util.List; + +/** + * 字典工具类 + * + * @author duteliang + */ +public class DictUtils +{ + /** + * 分隔符 + */ + public static final String SEPARATOR = ","; + + /** + * 设置字典缓存 + * + * @param key 参数键 + * @param dictDatas 字典数据列表 + */ + public static void setDictCache(String key, List dictDatas) + { + SpringUtils.getBean(RedisCache.class).setCacheObject(getCacheKey(key), dictDatas); + } + + /** + * 获取字典缓存 + * + * @param key 参数键 + * @return dictDatas 字典数据列表 + */ + public static List getDictCache(String key) + { + Object cacheObj = SpringUtils.getBean(RedisCache.class).getCacheObject(getCacheKey(key)); + if (Validator.isNotNull(cacheObj)) + { + List dictDatas = (List)cacheObj; + return dictDatas; + } + return null; + } + + /** + * 根据字典类型和字典值获取字典标签 + * + * @param dictType 字典类型 + * @param dictValue 字典值 + * @return 字典标签 + */ + public static String getDictLabel(String dictType, String dictValue) + { + return getDictLabel(dictType, dictValue, SEPARATOR); + } + + /** + * 根据字典类型和字典标签获取字典值 + * + * @param dictType 字典类型 + * @param dictLabel 字典标签 + * @return 字典值 + */ + public static String getDictValue(String dictType, String dictLabel) + { + return getDictValue(dictType, dictLabel, SEPARATOR); + } + + /** + * 根据字典类型和字典值获取字典标签 + * + * @param dictType 字典类型 + * @param dictValue 字典值 + * @param separator 分隔符 + * @return 字典标签 + */ + public static String getDictLabel(String dictType, String dictValue, String separator) + { + StringBuilder propertyString = new StringBuilder(); + List datas = getDictCache(dictType); + + if (StrUtil.containsAny(separator, dictValue) && CollUtil.isNotEmpty(datas)) + { + for (SysDictData dict : datas) + { + for (String value : dictValue.split(separator)) + { + if (value.equals(dict.getDictValue())) + { + propertyString.append(dict.getDictLabel() + separator); + break; + } + } + } + } + else + { + for (SysDictData dict : datas) + { + if (dictValue.equals(dict.getDictValue())) + { + return dict.getDictLabel(); + } + } + } + return StrUtil.strip(propertyString.toString(), null, separator); + } + + /** + * 根据字典类型和字典标签获取字典值 + * + * @param dictType 字典类型 + * @param dictLabel 字典标签 + * @param separator 分隔符 + * @return 字典值 + */ + public static String getDictValue(String dictType, String dictLabel, String separator) + { + StringBuilder propertyString = new StringBuilder(); + List datas = getDictCache(dictType); + + if (StrUtil.containsAny(separator, dictLabel) && CollUtil.isNotEmpty(datas)) + { + for (SysDictData dict : datas) + { + for (String label : dictLabel.split(separator)) + { + if (label.equals(dict.getDictLabel())) + { + propertyString.append(dict.getDictValue() + separator); + break; + } + } + } + } + else + { + for (SysDictData dict : datas) + { + if (dictLabel.equals(dict.getDictLabel())) + { + return dict.getDictValue(); + } + } + } + return StrUtil.strip(propertyString.toString(), null, separator); + } + + /** + * 删除指定字典缓存 + * + * @param key 字典键 + */ + public static void removeDictCache(String key) + { + SpringUtils.getBean(RedisCache.class).deleteObject(getCacheKey(key)); + } + + /** + * 清空字典缓存 + */ + public static void clearDictCache() + { + Collection keys = SpringUtils.getBean(RedisCache.class).keys(Constants.SYS_DICT_KEY + "*"); + SpringUtils.getBean(RedisCache.class).deleteObject(keys); + } + + /** + * 设置cache key + * + * @param configKey 参数键 + * @return 缓存键key + */ + public static String getCacheKey(String configKey) + { + return Constants.SYS_DICT_KEY + configKey; + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/utils/JsonUtils.java b/bashi-common/src/main/java/com/bashi/common/utils/JsonUtils.java new file mode 100644 index 0000000..64f9ab5 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/utils/JsonUtils.java @@ -0,0 +1,101 @@ +package com.bashi.common.utils; + +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * JSON 工具类 + * + * @author 芋道源码 + */ +public class JsonUtils { + + private static ObjectMapper objectMapper = new ObjectMapper(); + + /** + * 初始化 objectMapper 属性 + *

+ * 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean + * + * @param objectMapper ObjectMapper 对象 + */ + public static void init(ObjectMapper objectMapper) { + JsonUtils.objectMapper = objectMapper; + } + + public static String toJsonString(Object object) { + if (Validator.isEmpty(object)) { + return null; + } + try { + return objectMapper.writeValueAsString(object); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + public static T parseObject(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + try { + return objectMapper.readValue(text, clazz); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static T parseObject(byte[] bytes, Class clazz) { + if (ArrayUtil.isEmpty(bytes)) { + return null; + } + try { + return objectMapper.readValue(bytes, clazz); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static T parseObject(String text, TypeReference typeReference) { + if (StrUtil.isBlank(text)) { + return null; + } + try { + return objectMapper.readValue(text, typeReference); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static Map parseMap(String text) { + if (StrUtil.isBlank(text)) { + return null; + } + try { + return objectMapper.readValue(text, new TypeReference>() {}); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static List parseArray(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return new ArrayList<>(); + } + try { + return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/bashi-common/src/main/java/com/bashi/common/utils/MessageUtils.java b/bashi-common/src/main/java/com/bashi/common/utils/MessageUtils.java new file mode 100644 index 0000000..76afdbc --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/utils/MessageUtils.java @@ -0,0 +1,26 @@ +package com.bashi.common.utils; + +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import com.bashi.common.utils.spring.SpringUtils; + +/** + * 获取i18n资源文件 + * + * @author duteliang + */ +public class MessageUtils +{ + /** + * 根据消息键和参数 获取消息 委托给spring messageSource + * + * @param code 消息键 + * @param args 参数 + * @return 获取国际化翻译值 + */ + public static String message(String code, Object... args) + { + MessageSource messageSource = SpringUtils.getBean(MessageSource.class); + return messageSource.getMessage(code, args, LocaleContextHolder.getLocale()); + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/utils/PageUtils.java b/bashi-common/src/main/java/com/bashi/common/utils/PageUtils.java new file mode 100644 index 0000000..1edb748 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/utils/PageUtils.java @@ -0,0 +1,155 @@ +package com.bashi.common.utils; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpStatus; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.metadata.OrderItem; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.bashi.common.core.page.PagePlus; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.utils.sql.SqlUtil; +import org.springframework.beans.BeanUtils; +import org.springframework.context.annotation.Bean; + +import javax.naming.Name; +import java.util.List; +import java.util.function.Supplier; + +/** + * 分页工具 + * + * @author Lion Li + */ +public class PageUtils { + + /** + * 当前记录起始索引 + */ + public static final String PAGE_NUM = "pageNum"; + + /** + * 每页显示记录数 + */ + public static final String PAGE_SIZE = "pageSize"; + + /** + * 排序列 + */ + public static final String ORDER_BY_COLUMN = "orderByColumn"; + + /** + * 排序的方向 "desc" 或者 "asc". + */ + public static final String IS_ASC = "isAsc"; + + /** + * 当前记录起始索引 默认值 + */ + public static final int DEFAULT_PAGE_NUM = 1; + + /** + * 每页显示记录数 默认值 默认查全部 + */ + public static final int DEFAULT_PAGE_SIZE = Integer.MAX_VALUE; + + /** + * 构建 plus 分页对象 + * @param domain 实体 + * @param vo 实体 + * @return 分页对象 + */ + public static PagePlus buildPagePlus() { + Integer pageNum = ServletUtils.getParameterToInt(PAGE_NUM, DEFAULT_PAGE_NUM); + Integer pageSize = ServletUtils.getParameterToInt(PAGE_SIZE, DEFAULT_PAGE_SIZE); + String orderByColumn = ServletUtils.getParameter(ORDER_BY_COLUMN); + String isAsc = ServletUtils.getParameter(IS_ASC); + PagePlus page = new PagePlus<>(pageNum, pageSize); + if (StrUtil.isNotBlank(orderByColumn)) { + String orderBy = SqlUtil.escapeOrderBySql(orderByColumn); + if ("asc".equals(isAsc)) { + page.addOrder(OrderItem.asc(orderBy)); + } else if ("desc".equals(isAsc)) { + page.addOrder(OrderItem.desc(orderBy)); + } + } + return page; + } + + public static Page buildPage() { + return buildPage(null, null); + } + + /** + * 构建 MP 普通分页对象 + * @param domain 实体 + * @return 分页对象 + */ + public static Page buildPage(String defaultOrderByColumn, String defaultIsAsc) { + Integer pageNum = ServletUtils.getParameterToInt(PAGE_NUM, DEFAULT_PAGE_NUM); + Integer pageSize = ServletUtils.getParameterToInt(PAGE_SIZE, DEFAULT_PAGE_SIZE); + String orderByColumn = ServletUtils.getParameter(ORDER_BY_COLUMN, defaultOrderByColumn); + String isAsc = ServletUtils.getParameter(IS_ASC, defaultIsAsc); + // 兼容前端排序类型 + if ("ascending".equals(isAsc)) { + isAsc = "asc"; + } else if ("descending".equals(isAsc)) { + isAsc = "desc"; + } + Page page = new Page<>(pageNum, pageSize); + if (StrUtil.isNotBlank(orderByColumn)) { + String orderBy = SqlUtil.escapeOrderBySql(orderByColumn); + orderBy = StrUtil.toUnderlineCase(orderBy); + if ("asc".equals(isAsc)) { + page.addOrder(OrderItem.asc(orderBy)); + } else if ("desc".equals(isAsc)) { + page.addOrder(OrderItem.desc(orderBy)); + } + } + return page; + } + + public static TableDataInfo buildDataInfo(PagePlus page) { + TableDataInfo rspData = new TableDataInfo<>(); + rspData.setCode(HttpStatus.HTTP_OK); + rspData.setMsg("查询成功"); + rspData.setRows(page.getRecordsVo()); + rspData.setTotal(page.getTotal()); + return rspData; + } + + public static TableDataInfo buildDataInfo(IPage page) { + TableDataInfo rspData = new TableDataInfo<>(); + rspData.setCode(HttpStatus.HTTP_OK); + rspData.setMsg("查询成功"); + rspData.setRows(page.getRecords()); + rspData.setTotal(page.getTotal()); + return rspData; + } + + public static IPage transPage(IPage page, Supplier targetSupplier) { + IPage rspData = new Page<>(page.getCurrent(),page.getSize(),page.getTotal()); + List listTo = BeanConvertUtil.convertListTo(page.getRecords(), targetSupplier); + rspData.setRecords(listTo); + return rspData; + } + + public static TableDataInfo buildDataInfo(List list) { + TableDataInfo rspData = new TableDataInfo<>(); + rspData.setCode(HttpStatus.HTTP_OK); + rspData.setMsg("查询成功"); + rspData.setRows(list); + rspData.setTotal(list.size()); + return rspData; + } + + public static TableDataInfo buildDataInfo(IPage page, Supplier targetSupplier) { + TableDataInfo rspData = new TableDataInfo<>(); + rspData.setCode(HttpStatus.HTTP_OK); + rspData.setMsg("查询成功"); + List listTo = BeanConvertUtil.convertListTo(page.getRecords(), targetSupplier); + rspData.setRows(listTo); + rspData.setTotal(page.getTotal()); + return rspData; + } + +} diff --git a/bashi-common/src/main/java/com/bashi/common/utils/SecurityUtils.java b/bashi-common/src/main/java/com/bashi/common/utils/SecurityUtils.java new file mode 100644 index 0000000..7692b0b --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/utils/SecurityUtils.java @@ -0,0 +1,96 @@ +package com.bashi.common.utils; + +import cn.hutool.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import com.bashi.common.core.domain.model.LoginUser; +import com.bashi.common.exception.CustomException; + +/** + * 安全服务工具类 + * + * @author duteliang + */ +public class SecurityUtils { + /** + * 获取用户账户 + **/ + public static String getUsername() { + try { + return getLoginUser().getUsername(); + } catch (Exception e) { + throw new CustomException("获取用户账户异常", HttpStatus.HTTP_UNAUTHORIZED); + } + } + + /** + * 获取用户账户 + **/ + public static Long getUserId() { + try { + return getLoginUser().getUser().getUserId(); + } catch (Exception e) { + throw new CustomException("获取用户账户异常", HttpStatus.HTTP_UNAUTHORIZED); + } + } + + /** + * 获取用户 + **/ + public static LoginUser getLoginUser() { + try { + return (LoginUser) getAuthentication().getPrincipal(); + } catch (Exception e) { + throw new CustomException("获取用户信息异常", HttpStatus.HTTP_UNAUTHORIZED); + } + } + + public static LoginUser getLoginUserNoException() { + try { + return (LoginUser) getAuthentication().getPrincipal(); + } catch (Exception e) { + return null; + } + } + + /** + * 获取Authentication + */ + public static Authentication getAuthentication() { + return SecurityContextHolder.getContext().getAuthentication(); + } + + /** + * 生成BCryptPasswordEncoder密码 + * + * @param password 密码 + * @return 加密字符串 + */ + public static String encryptPassword(String password) { + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + return passwordEncoder.encode(password); + } + + /** + * 判断密码是否相同 + * + * @param rawPassword 真实密码 + * @param encodedPassword 加密后字符 + * @return 结果 + */ + public static boolean matchesPassword(String rawPassword, String encodedPassword) { + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + return passwordEncoder.matches(rawPassword, encodedPassword); + } + + /** + * 是否为管理员 + * + * @param userId 用户ID + * @return 结果 + */ + public static boolean isAdmin(Long userId) { + return userId != null && 1L == userId; + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/utils/ServletUtils.java b/bashi-common/src/main/java/com/bashi/common/utils/ServletUtils.java new file mode 100644 index 0000000..d68279b --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/utils/ServletUtils.java @@ -0,0 +1,130 @@ +package com.bashi.common.utils; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.servlet.ServletUtil; +import cn.hutool.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * 客户端工具类 + * + * @author duteliang + */ +public class ServletUtils extends ServletUtil { + /** + * 获取String参数 + */ + public static String getParameter(String name) { + return getRequest().getParameter(name); + } + + /** + * 获取String参数 + */ + public static String getParameter(String name, String defaultValue) { + return Convert.toStr(getRequest().getParameter(name), defaultValue); + } + + /** + * 获取Integer参数 + */ + public static Integer getParameterToInt(String name) { + return Convert.toInt(getRequest().getParameter(name)); + } + + /** + * 获取Integer参数 + */ + public static Integer getParameterToInt(String name, Integer defaultValue) { + return Convert.toInt(getRequest().getParameter(name), defaultValue); + } + + /** + * 获取request + */ + public static HttpServletRequest getRequest() { + return getRequestAttributes().getRequest(); + } + + /** + * 获取response + */ + public static HttpServletResponse getResponse() { + return getRequestAttributes().getResponse(); + } + + /** + * 获取session + */ + public static HttpSession getSession() { + return getRequest().getSession(); + } + + public static ServletRequestAttributes getRequestAttributes() { + RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); + return (ServletRequestAttributes) attributes; + } + + /** + * 将字符串渲染到客户端 + * + * @param response 渲染对象 + * @param string 待渲染的字符串 + * @return null + */ + public static String renderString(HttpServletResponse response, String string) { + try { + response.setStatus(HttpStatus.HTTP_OK); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.toString()); + response.getWriter().print(string); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + /** + * 是否是Ajax异步请求 + * + * @param request + */ + public static boolean isAjaxRequest(HttpServletRequest request) { + + String accept = request.getHeader("accept"); + if (accept != null && accept.indexOf("application/json") != -1) { + return true; + } + + String xRequestedWith = request.getHeader("X-Requested-With"); + if (xRequestedWith != null && xRequestedWith.indexOf("XMLHttpRequest") != -1) { + return true; + } + + String uri = request.getRequestURI(); + if (StrUtil.equalsAnyIgnoreCase(uri, ".json", ".xml")) { + return true; + } + + String ajax = request.getParameter("__ajax"); + if (StrUtil.equalsAnyIgnoreCase(ajax, "json", "xml")) { + return true; + } + return false; + } + + public static String getClientIP() { + return getClientIP(getRequest()); + } + +} diff --git a/bashi-common/src/main/java/com/bashi/common/utils/Threads.java b/bashi-common/src/main/java/com/bashi/common/utils/Threads.java new file mode 100644 index 0000000..a2aa09e --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/utils/Threads.java @@ -0,0 +1,99 @@ +package com.bashi.common.utils; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 线程相关工具类. + * + * @author duteliang + */ +public class Threads +{ + private static final Logger logger = LoggerFactory.getLogger(Threads.class); + + /** + * sleep等待,单位为毫秒 + */ + public static void sleep(long milliseconds) + { + try + { + Thread.sleep(milliseconds); + } + catch (InterruptedException e) + { + return; + } + } + + /** + * 停止线程池 + * 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务. + * 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数. + * 如果仍人超時,則強制退出. + * 另对在shutdown时线程本身被调用中断做了处理. + */ + public static void shutdownAndAwaitTermination(ExecutorService pool) + { + if (pool != null && !pool.isShutdown()) + { + pool.shutdown(); + try + { + if (!pool.awaitTermination(120, TimeUnit.SECONDS)) + { + pool.shutdownNow(); + if (!pool.awaitTermination(120, TimeUnit.SECONDS)) + { + logger.info("Pool did not terminate"); + } + } + } + catch (InterruptedException ie) + { + pool.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } + + /** + * 打印线程异常信息 + */ + public static void printException(Runnable r, Throwable t) + { + if (t == null && r instanceof Future) + { + try + { + Future future = (Future) r; + if (future.isDone()) + { + future.get(); + } + } + catch (CancellationException ce) + { + t = ce; + } + catch (ExecutionException ee) + { + t = ee.getCause(); + } + catch (InterruptedException ie) + { + Thread.currentThread().interrupt(); + } + } + if (t != null) + { + logger.error(t.getMessage(), t); + } + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/utils/file/FileTypeUtils.java b/bashi-common/src/main/java/com/bashi/common/utils/file/FileTypeUtils.java new file mode 100644 index 0000000..8da3452 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/utils/file/FileTypeUtils.java @@ -0,0 +1,76 @@ +package com.bashi.common.utils.file; + +import java.io.File; +import org.apache.commons.lang3.StringUtils; + +/** + * 文件类型工具类 + * + * @author duteliang + */ +public class FileTypeUtils +{ + /** + * 获取文件类型 + *

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

+ * 例如: duteliang.txt, 返回: txt + * + * @param fileName 文件名 + * @return 后缀(不含".") + */ + public static String getFileType(String fileName) + { + int separatorIndex = fileName.lastIndexOf("."); + if (separatorIndex < 0) + { + return ""; + } + return fileName.substring(separatorIndex + 1).toLowerCase(); + } + + /** + * 获取文件类型 + * + * @param photoByte 文件字节码 + * @return 后缀(不含".") + */ + public static String getFileExtendName(byte[] photoByte) + { + String strFileExtendName = "JPG"; + if ((photoByte[0] == 71) && (photoByte[1] == 73) && (photoByte[2] == 70) && (photoByte[3] == 56) + && ((photoByte[4] == 55) || (photoByte[4] == 57)) && (photoByte[5] == 97)) + { + strFileExtendName = "GIF"; + } + else if ((photoByte[6] == 74) && (photoByte[7] == 70) && (photoByte[8] == 73) && (photoByte[9] == 70)) + { + strFileExtendName = "JPG"; + } + else if ((photoByte[0] == 66) && (photoByte[1] == 77)) + { + strFileExtendName = "BMP"; + } + else if ((photoByte[1] == 80) && (photoByte[2] == 78) && (photoByte[3] == 71)) + { + strFileExtendName = "PNG"; + } + return strFileExtendName; + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/utils/file/FileUploadUtils.java b/bashi-common/src/main/java/com/bashi/common/utils/file/FileUploadUtils.java new file mode 100644 index 0000000..657c2c4 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/utils/file/FileUploadUtils.java @@ -0,0 +1,239 @@ +package com.bashi.common.utils.file; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import com.bashi.common.config.BsConfig; +import com.bashi.common.constant.Constants; +import com.bashi.common.exception.file.FileNameLengthLimitExceededException; +import com.bashi.common.exception.file.FileSizeLimitExceededException; +import com.bashi.common.exception.file.InvalidExtensionException; +import com.bashi.common.utils.DateUtils; +import org.apache.commons.io.FilenameUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; + +/** + * 文件上传工具类 + * + * @author duteliang + */ +public class FileUploadUtils +{ + /** + * 默认大小 50M + */ + public static final long DEFAULT_MAX_SIZE = 50 * 1024 * 1024; + + /** + * 默认的文件名最大长度 100 + */ + public static final int DEFAULT_FILE_NAME_LENGTH = 100; + + /** + * 默认上传的地址 + */ + private static String defaultBaseDir = BsConfig.getProfile(); + + public static void setDefaultBaseDir(String defaultBaseDir) + { + FileUploadUtils.defaultBaseDir = defaultBaseDir; + } + + public static String getDefaultBaseDir() + { + return defaultBaseDir; + } + + /** + * 以默认配置进行文件上传 + * + * @param file 上传的文件 + * @return 文件名称 + * @throws Exception + */ + public static final String upload(MultipartFile file) throws IOException + { + try + { + return upload(getDefaultBaseDir(), file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); + } + catch (Exception e) + { + throw new IOException(e.getMessage(), e); + } + } + + /** + * 根据文件路径上传 + * + * @param baseDir 相对应用的基目录 + * @param file 上传的文件 + * @return 文件名称 + * @throws IOException + */ + public static final String upload(String baseDir, MultipartFile file) throws IOException + { + try + { + return upload(baseDir, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); + } + catch (Exception e) + { + throw new IOException(e.getMessage(), e); + } + } + + /** + * 文件上传 + * + * @param baseDir 相对应用的基目录 + * @param file 上传的文件 + * @param allowedExtension 上传文件类型 + * @return 返回上传成功的文件名 + * @throws FileSizeLimitExceededException 如果超出最大大小 + * @throws FileNameLengthLimitExceededException 文件名太长 + * @throws IOException 比如读写文件出错时 + * @throws InvalidExtensionException 文件校验异常 + */ + public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension) + throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException, + InvalidExtensionException + { + int fileNamelength = file.getOriginalFilename().length(); + if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH) + { + throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH); + } + + assertAllowed(file, allowedExtension); + + String fileName = extractFilename(file); + + File desc = getAbsoluteFile(baseDir, fileName); + desc = FileUtil.touch(desc); + FileUtil.writeFromStream(file.getInputStream(), desc); + String pathFileName = getPathFileName(baseDir, fileName); + return pathFileName; + } + + /** + * 编码文件名 + */ + public static final String extractFilename(MultipartFile file) + { + String fileName = file.getOriginalFilename(); + String extension = getExtension(file); + fileName = DateUtils.datePath() + "/" + IdUtil.fastUUID() + "." + extension; + return fileName; + } + + private static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException + { + File desc = new File(uploadDir + File.separator + fileName); + + if (!desc.exists()) + { + if (!desc.getParentFile().exists()) + { + desc.getParentFile().mkdirs(); + } + } + return desc; + } + + private static final String getPathFileName(String uploadDir, String fileName) throws IOException + { + int dirLastIndex = BsConfig.getProfile().length() + 1; + String currentDir = StrUtil.subSuf(uploadDir, dirLastIndex); + String pathFileName = Constants.RESOURCE_PREFIX + "/" + currentDir + "/" + fileName; + return pathFileName; + } + + /** + * 文件大小校验 + * + * @param file 上传的文件 + * @return + * @throws FileSizeLimitExceededException 如果超出最大大小 + * @throws InvalidExtensionException + */ + public static final void assertAllowed(MultipartFile file, String[] allowedExtension) + throws FileSizeLimitExceededException, InvalidExtensionException + { + long size = file.getSize(); + if (DEFAULT_MAX_SIZE != -1 && size > DEFAULT_MAX_SIZE) + { + throw new FileSizeLimitExceededException(DEFAULT_MAX_SIZE / 1024 / 1024); + } + + String fileName = file.getOriginalFilename(); + String extension = getExtension(file); + if (allowedExtension != null && !isAllowedExtension(extension, allowedExtension)) + { + if (allowedExtension == MimeTypeUtils.IMAGE_EXTENSION) + { + throw new InvalidExtensionException.InvalidImageExtensionException(allowedExtension, extension, + fileName); + } + else if (allowedExtension == MimeTypeUtils.FLASH_EXTENSION) + { + throw new InvalidExtensionException.InvalidFlashExtensionException(allowedExtension, extension, + fileName); + } + else if (allowedExtension == MimeTypeUtils.MEDIA_EXTENSION) + { + throw new InvalidExtensionException.InvalidMediaExtensionException(allowedExtension, extension, + fileName); + } + else if (allowedExtension == MimeTypeUtils.VIDEO_EXTENSION) + { + throw new InvalidExtensionException.InvalidVideoExtensionException(allowedExtension, extension, + fileName); + } + else + { + throw new InvalidExtensionException(allowedExtension, extension, fileName); + } + } + + } + + /** + * 判断MIME类型是否是允许的MIME类型 + * + * @param extension + * @param allowedExtension + * @return + */ + public static final boolean isAllowedExtension(String extension, String[] allowedExtension) + { + for (String str : allowedExtension) + { + if (str.equalsIgnoreCase(extension)) + { + return true; + } + } + return false; + } + + /** + * 获取文件名的后缀 + * + * @param file 表单文件 + * @return 后缀名 + */ + public static final String getExtension(MultipartFile file) + { + String extension = FilenameUtils.getExtension(file.getOriginalFilename()); + if (Validator.isEmpty(extension)) + { + extension = MimeTypeUtils.getExtension(file.getContentType()); + } + return extension; + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/utils/file/FileUtils.java b/bashi-common/src/main/java/com/bashi/common/utils/file/FileUtils.java new file mode 100644 index 0000000..82b82e4 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/utils/file/FileUtils.java @@ -0,0 +1,125 @@ +package com.bashi.common.utils.file; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +/** + * 文件处理工具类 + * + * @author duteliang + */ +public class FileUtils extends FileUtil +{ + public static String FILENAME_PATTERN = "[a-zA-Z0-9_\\-\\|\\.\\u4e00-\\u9fa5]+"; + + /** + * 文件名称验证 + * + * @param filename 文件名称 + * @return true 正常 false 非法 + */ + public static boolean isValidFilename(String filename) + { + return filename.matches(FILENAME_PATTERN); + } + + /** + * 检查文件是否可下载 + * + * @param resource 需要下载的文件 + * @return true 正常 false 非法 + */ + public static boolean checkAllowDownload(String resource) + { + // 禁止目录上跳级别 + if (StrUtil.contains(resource, "..")) + { + return false; + } + + // 检查允许下载的文件规则 + if (ArrayUtil.contains(MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION, FileTypeUtils.getFileType(resource))) + { + return true; + } + + // 不在允许下载的文件规则 + return false; + } + + /** + * 下载文件名重新编码 + * + * @param request 请求对象 + * @param fileName 文件名 + * @return 编码后的文件名 + */ + public static String setFileDownloadHeader(HttpServletRequest request, String fileName) throws UnsupportedEncodingException + { + final String agent = request.getHeader("USER-AGENT"); + String filename = fileName; + if (agent.contains("MSIE")) + { + // IE浏览器 + filename = URLEncoder.encode(filename, StandardCharsets.UTF_8.toString()); + filename = filename.replace("+", " "); + } + else if (agent.contains("Firefox")) + { + // 火狐浏览器 + filename = new String(fileName.getBytes(), "ISO8859-1"); + } + else if (agent.contains("Chrome")) + { + // google浏览器 + filename = URLEncoder.encode(filename, StandardCharsets.UTF_8.toString()); + } + else + { + // 其它浏览器 + filename = URLEncoder.encode(filename, StandardCharsets.UTF_8.toString()); + } + return filename; + } + + /** + * 下载文件名重新编码 + * + * @param response 响应对象 + * @param realFileName 真实文件名 + * @return + */ + public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) throws UnsupportedEncodingException + { + String percentEncodedFileName = percentEncode(realFileName); + + StringBuilder contentDispositionValue = new StringBuilder(); + contentDispositionValue.append("attachment; filename=") + .append(percentEncodedFileName) + .append(";") + .append("filename*=") + .append("utf-8''") + .append(percentEncodedFileName); + + response.setHeader("Content-disposition", contentDispositionValue.toString()); + } + + /** + * 百分号编码工具方法 + * + * @param s 需要百分号编码的字符串 + * @return 百分号编码后的字符串 + */ + public static String percentEncode(String s) throws UnsupportedEncodingException + { + String encode = URLEncoder.encode(s, StandardCharsets.UTF_8.toString()); + return encode.replaceAll("\\+", "%20"); + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/utils/file/ImageUtils.java b/bashi-common/src/main/java/com/bashi/common/utils/file/ImageUtils.java new file mode 100644 index 0000000..79d7948 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/utils/file/ImageUtils.java @@ -0,0 +1,102 @@ +package com.bashi.common.utils.file; + +import cn.hutool.core.util.StrUtil; +import com.bashi.common.config.BsConfig; +import com.bashi.common.constant.Constants; +import org.apache.poi.util.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.Arrays; + +/** + * 图片处理工具类 + * + * @author duteliang + */ +public class ImageUtils +{ + private static final Logger log = LoggerFactory.getLogger(ImageUtils.class); + + public static byte[] getImage(String imagePath) + { + InputStream is = getFile(imagePath); + try + { + return IOUtils.toByteArray(is); + } + catch (Exception e) + { + log.error("图片加载异常 {}", e); + return null; + } + finally + { + IOUtils.closeQuietly(is); + } + } + + public static InputStream getFile(String imagePath) + { + try + { + byte[] result = readFile(imagePath); + result = Arrays.copyOf(result, result.length); + return new ByteArrayInputStream(result); + } + catch (Exception e) + { + log.error("获取图片异常 {}", e); + } + return null; + } + + /** + * 读取文件为字节数据 + * + * @param key 地址 + * @return 字节数据 + */ + public static byte[] readFile(String url) + { + InputStream in = null; + ByteArrayOutputStream baos = null; + try + { + if (url.startsWith("http")) + { + // 网络地址 + URL urlObj = new URL(url); + URLConnection urlConnection = urlObj.openConnection(); + urlConnection.setConnectTimeout(30 * 1000); + urlConnection.setReadTimeout(60 * 1000); + urlConnection.setDoInput(true); + in = urlConnection.getInputStream(); + } + else + { + // 本机地址 + String localPath = BsConfig.getProfile(); + String downloadPath = localPath + StrUtil.subAfter(url, Constants.RESOURCE_PREFIX,false); + in = new FileInputStream(downloadPath); + } + return IOUtils.toByteArray(in); + } + catch (Exception e) + { + log.error("获取文件路径异常 {}", e); + return null; + } + finally + { + IOUtils.closeQuietly(in); + IOUtils.closeQuietly(baos); + } + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/utils/file/MimeTypeUtils.java b/bashi-common/src/main/java/com/bashi/common/utils/file/MimeTypeUtils.java new file mode 100644 index 0000000..b5211c0 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/utils/file/MimeTypeUtils.java @@ -0,0 +1,59 @@ +package com.bashi.common.utils.file; + +/** + * 媒体类型工具类 + * + * @author duteliang + */ +public class MimeTypeUtils +{ + public static final String IMAGE_PNG = "image/png"; + + public static final String IMAGE_JPG = "image/jpg"; + + public static final String IMAGE_JPEG = "image/jpeg"; + + public static final String IMAGE_BMP = "image/bmp"; + + public static final String IMAGE_GIF = "image/gif"; + + public static final String[] IMAGE_EXTENSION = { "bmp", "gif", "jpg", "jpeg", "png" }; + + public static final String[] FLASH_EXTENSION = { "swf", "flv" }; + + public static final String[] MEDIA_EXTENSION = { "swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg", + "asf", "rm", "rmvb" }; + + public static final String[] VIDEO_EXTENSION = { "mp4", "avi", "rmvb" }; + + public static final String[] DEFAULT_ALLOWED_EXTENSION = { + // 图片 + "bmp", "gif", "jpg", "jpeg", "png", + // word excel powerpoint + "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt", + // 压缩文件 + "rar", "zip", "gz", "bz2", + // 视频格式 + "mp4", "avi", "rmvb", + // pdf + "pdf" }; + + public static String getExtension(String prefix) + { + switch (prefix) + { + case IMAGE_PNG: + return "png"; + case IMAGE_JPG: + return "jpg"; + case IMAGE_JPEG: + return "jpeg"; + case IMAGE_BMP: + return "bmp"; + case IMAGE_GIF: + return "gif"; + default: + return ""; + } + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/utils/ip/AddressUtils.java b/bashi-common/src/main/java/com/bashi/common/utils/ip/AddressUtils.java new file mode 100644 index 0000000..0779324 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/utils/ip/AddressUtils.java @@ -0,0 +1,55 @@ +package com.bashi.common.utils.ip; + +import cn.hutool.core.net.NetUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HtmlUtil; +import cn.hutool.http.HttpUtil; +import com.bashi.common.config.BsConfig; +import com.bashi.common.constant.Constants; +import com.bashi.common.utils.JsonUtils; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +/** + * 获取地址类 + * + * @author duteliang + */ +@Slf4j +public class AddressUtils { + + // IP地址查询 + public static final String IP_URL = "http://whois.pconline.com.cn/ipJson.jsp"; + + // 未知地址 + public static final String UNKNOWN = "XX XX"; + + public static String getRealAddressByIP(String ip) { + String address = UNKNOWN; + // 内网不查询 + ip = "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : HtmlUtil.cleanHtmlTag(ip); + if (NetUtil.isInnerIP(ip)) { + return "内网IP"; + } + if (BsConfig.isAddressEnabled()) { + try { + String rspStr = HttpUtil.createGet(IP_URL) + .body("ip=" + ip + "&json=true", Constants.GBK) + .execute() + .body(); + if (StrUtil.isEmpty(rspStr)) { + log.error("获取地理位置异常 {}", ip); + return UNKNOWN; + } + Map obj = JsonUtils.parseMap(rspStr); + String region = obj.get("pro"); + String city = obj.get("city"); + return String.format("%s %s", region, city); + } catch (Exception e) { + log.error("获取地理位置异常 {}", ip); + } + } + return address; + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/utils/poi/ExcelUtil.java b/bashi-common/src/main/java/com/bashi/common/utils/poi/ExcelUtil.java new file mode 100644 index 0000000..eac8ce0 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/utils/poi/ExcelUtil.java @@ -0,0 +1,1072 @@ +package com.bashi.common.utils.poi; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.StrUtil; +import com.bashi.common.annotation.Excel; +import com.bashi.common.annotation.Excel.ColumnType; +import com.bashi.common.annotation.Excel.Type; +import com.bashi.common.annotation.Excels; +import com.bashi.common.config.BsConfig; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.exception.CustomException; +import com.bashi.common.utils.DateUtils; +import com.bashi.common.utils.DictUtils; +import com.bashi.common.utils.file.FileTypeUtils; +import com.bashi.common.utils.file.ImageUtils; +import com.bashi.common.utils.reflect.ReflectUtils; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.ss.util.CellRangeAddressList; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; +import org.apache.poi.xssf.usermodel.XSSFClientAnchor; +import org.apache.poi.xssf.usermodel.XSSFDataValidation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.lang.reflect.Field; +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Excel相关处理 + * + * @author duteliang + */ +public class ExcelUtil +{ + private static final Logger log = LoggerFactory.getLogger(ExcelUtil.class); + + /** + * Excel sheet最大行数,默认65536 + */ + public static final int sheetSize = 65536; + + /** + * 工作表名称 + */ + private String sheetName; + + /** + * 导出类型(EXPORT:导出数据;IMPORT:导入模板) + */ + private Type type; + + /** + * 工作薄对象 + */ + private Workbook wb; + + /** + * 工作表对象 + */ + private Sheet sheet; + + /** + * 样式列表 + */ + private Map styles; + + /** + * 导入导出数据列表 + */ + private List list; + + /** + * 注解列表 + */ + private List fields; + + /** + * 最大高度 + */ + private short maxHeight; + + /** + * 统计列表 + */ + private Map statistics = new HashMap(); + + /** + * 数字格式 + */ + private static final DecimalFormat DOUBLE_FORMAT = new DecimalFormat("######0.00"); + + /** + * 实体对象 + */ + public Class clazz; + + public ExcelUtil(Class clazz) + { + this.clazz = clazz; + } + + public void init(List list, String sheetName, Type type) + { + if (list == null) + { + list = new ArrayList(); + } + this.list = list; + this.sheetName = sheetName; + this.type = type; + createExcelField(); + createWorkbook(); + } + + /** + * 对excel表单默认第一个索引名转换成list + * + * @param is 输入流 + * @return 转换后集合 + */ + public List importExcel(InputStream is) throws Exception + { + return importExcel(StrUtil.EMPTY, is); + } + + /** + * 对excel表单指定表格索引名转换成list + * + * @param sheetName 表格索引名 + * @param is 输入流 + * @return 转换后集合 + */ + public List importExcel(String sheetName, InputStream is) throws Exception + { + this.type = Type.IMPORT; + this.wb = WorkbookFactory.create(is); + List list = new ArrayList(); + Sheet sheet = null; + if (Validator.isNotEmpty(sheetName)) + { + // 如果指定sheet名,则取指定sheet中的内容. + sheet = wb.getSheet(sheetName); + } + else + { + // 如果传入的sheet名不存在则默认指向第1个sheet. + sheet = wb.getSheetAt(0); + } + + if (sheet == null) + { + throw new IOException("文件sheet不存在"); + } + + int rows = sheet.getPhysicalNumberOfRows(); + + if (rows > 0) + { + // 定义一个map用于存放excel列的序号和field. + Map cellMap = new HashMap(); + // 获取表头 + Row heard = sheet.getRow(0); + for (int i = 0; i < heard.getPhysicalNumberOfCells(); i++) + { + Cell cell = heard.getCell(i); + if (Validator.isNotNull(cell)) + { + String value = this.getCellValue(heard, i).toString(); + cellMap.put(value, i); + } + else + { + cellMap.put(null, i); + } + } + // 有数据时才处理 得到类的所有field. + Field[] allFields = clazz.getDeclaredFields(); + // 定义一个map用于存放列的序号和field. + Map fieldsMap = new HashMap(); + for (int col = 0; col < allFields.length; col++) + { + Field field = allFields[col]; + Excel attr = field.getAnnotation(Excel.class); + if (attr != null && (attr.type() == Type.ALL || attr.type() == type)) + { + // 设置类的私有字段属性可访问. + field.setAccessible(true); + Integer column = cellMap.get(attr.name()); + if (column != null) + { + fieldsMap.put(column, field); + } + } + } + for (int i = 1; i < rows; i++) + { + // 从第2行开始取数据,默认第一行是表头. + Row row = sheet.getRow(i); + if(row == null) + { + continue; + } + T entity = null; + for (Map.Entry entry : fieldsMap.entrySet()) + { + Object val = this.getCellValue(row, entry.getKey()); + + // 如果不存在实例则新建. + entity = (entity == null ? clazz.newInstance() : entity); + // 从map中得到对应列的field. + Field field = fieldsMap.get(entry.getKey()); + // 取得类型,并根据对象类型设置值. + Class fieldType = field.getType(); + if (String.class == fieldType) + { + String s = Convert.toStr(val); + if (StrUtil.endWith(s, ".0")) + { + val = StrUtil.subBefore(s, ".0",false); + } + else + { + String dateFormat = field.getAnnotation(Excel.class).dateFormat(); + if (Validator.isNotEmpty(dateFormat)) + { + val = DateUtils.parseDateToStr(dateFormat, (Date) val); + } + else + { + val = Convert.toStr(val); + } + } + } + else if ((Integer.TYPE == fieldType || Integer.class == fieldType) && Validator.isNumber(Convert.toStr(val))) + { + val = Convert.toInt(val); + } + else if (Long.TYPE == fieldType || Long.class == fieldType) + { + val = Convert.toLong(val); + } + else if (Double.TYPE == fieldType || Double.class == fieldType) + { + val = Convert.toDouble(val); + } + else if (Float.TYPE == fieldType || Float.class == fieldType) + { + val = Convert.toFloat(val); + } + else if (BigDecimal.class == fieldType) + { + val = Convert.toBigDecimal(val); + } + else if (Date.class == fieldType) + { + if (val instanceof String) + { + val = DateUtils.parseDate(val); + } + else if (val instanceof Double) + { + val = DateUtil.getJavaDate((Double) val); + } + } + else if (Boolean.TYPE == fieldType || Boolean.class == fieldType) + { + val = Convert.toBool(val, false); + } + if (Validator.isNotNull(fieldType)) + { + Excel attr = field.getAnnotation(Excel.class); + String propertyName = field.getName(); + if (Validator.isNotEmpty(attr.targetAttr())) + { + propertyName = field.getName() + "." + attr.targetAttr(); + } + else if (Validator.isNotEmpty(attr.readConverterExp())) + { + val = reverseByExp(Convert.toStr(val), attr.readConverterExp(), attr.separator()); + } + else if (Validator.isNotEmpty(attr.dictType())) + { + val = reverseDictByExp(Convert.toStr(val), attr.dictType(), attr.separator()); + } + ReflectUtils.invokeSetter(entity, propertyName, val); + } + } + list.add(entity); + } + } + return list; + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @return 结果 + */ + public AjaxResult exportExcel(List list, String sheetName) + { + this.init(list, sheetName, Type.EXPORT); + return exportExcel(); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @return 结果 + */ + public AjaxResult importTemplateExcel(String sheetName) + { + this.init(null, sheetName, Type.IMPORT); + return exportExcel(); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @return 结果 + */ + public AjaxResult exportExcel() + { + OutputStream out = null; + try + { + // 取出一共有多少个sheet. + double sheetNo = Math.ceil(list.size() / sheetSize); + for (int index = 0; index <= sheetNo; index++) + { + createSheet(sheetNo, index); + + // 产生一行 + Row row = sheet.createRow(0); + int column = 0; + // 写入各个字段的列头名称 + for (Object[] os : fields) + { + Excel excel = (Excel) os[1]; + this.createCell(excel, row, column++); + } + if (Type.EXPORT.equals(type)) + { + fillExcelData(index, row); + addStatisticsRow(); + } + } + String filename = encodingFilename(sheetName); + out = new FileOutputStream(getAbsoluteFile(filename)); + wb.write(out); + return AjaxResult.success(filename); + } + catch (Exception e) + { + log.error("导出Excel异常{}", e.getMessage()); + throw new CustomException("导出Excel失败,请联系网站管理员!"); + } + finally + { + if (wb != null) + { + try + { + wb.close(); + } + catch (IOException e1) + { + e1.printStackTrace(); + } + } + if (out != null) + { + try + { + out.close(); + } + catch (IOException e1) + { + e1.printStackTrace(); + } + } + } + } + + /** + * 填充excel数据 + * + * @param index 序号 + * @param row 单元格行 + */ + public void fillExcelData(int index, Row row) + { + int startNo = index * sheetSize; + int endNo = Math.min(startNo + sheetSize, list.size()); + for (int i = startNo; i < endNo; i++) + { + row = sheet.createRow(i + 1 - startNo); + // 得到导出对象. + T vo = (T) list.get(i); + int column = 0; + for (Object[] os : fields) + { + Field field = (Field) os[0]; + Excel excel = (Excel) os[1]; + // 设置实体类私有属性可访问 + field.setAccessible(true); + this.addCell(excel, row, vo, field, column++); + } + } + } + + /** + * 创建表格样式 + * + * @param wb 工作薄对象 + * @return 样式列表 + */ + private Map createStyles(Workbook wb) + { + // 写入各条记录,每条记录对应excel表中的一行 + Map styles = new HashMap(); + CellStyle style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setBorderRight(BorderStyle.THIN); + style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderLeft(BorderStyle.THIN); + style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderTop(BorderStyle.THIN); + style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderBottom(BorderStyle.THIN); + style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + Font dataFont = wb.createFont(); + dataFont.setFontName("Arial"); + dataFont.setFontHeightInPoints((short) 10); + style.setFont(dataFont); + styles.put("data", style); + + style = wb.createCellStyle(); + style.cloneStyleFrom(styles.get("data")); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setFillForegroundColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + Font headerFont = wb.createFont(); + headerFont.setFontName("Arial"); + headerFont.setFontHeightInPoints((short) 10); + headerFont.setBold(true); + headerFont.setColor(IndexedColors.WHITE.getIndex()); + style.setFont(headerFont); + styles.put("header", style); + + style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + Font totalFont = wb.createFont(); + totalFont.setFontName("Arial"); + totalFont.setFontHeightInPoints((short) 10); + style.setFont(totalFont); + styles.put("total", style); + + style = wb.createCellStyle(); + style.cloneStyleFrom(styles.get("data")); + style.setAlignment(HorizontalAlignment.LEFT); + styles.put("data1", style); + + style = wb.createCellStyle(); + style.cloneStyleFrom(styles.get("data")); + style.setAlignment(HorizontalAlignment.CENTER); + styles.put("data2", style); + + style = wb.createCellStyle(); + style.cloneStyleFrom(styles.get("data")); + style.setAlignment(HorizontalAlignment.RIGHT); + styles.put("data3", style); + + return styles; + } + + /** + * 创建单元格 + */ + public Cell createCell(Excel attr, Row row, int column) + { + // 创建列 + Cell cell = row.createCell(column); + // 写入列信息 + cell.setCellValue(attr.name()); + setDataValidation(attr, row, column); + cell.setCellStyle(styles.get("header")); + return cell; + } + + /** + * 设置单元格信息 + * + * @param value 单元格值 + * @param attr 注解相关 + * @param cell 单元格信息 + */ + public void setCellVo(Object value, Excel attr, Cell cell) + { + if (ColumnType.STRING == attr.cellType()) + { + cell.setCellValue(Validator.isNull(value) ? attr.defaultValue() : value + attr.suffix()); + } + else if (ColumnType.NUMERIC == attr.cellType()) + { + if (Validator.isNotNull(value)) + { + cell.setCellValue(StrUtil.contains(Convert.toStr(value), ".") ? Convert.toDouble(value) : Convert.toInt(value)); + } + } + else if (ColumnType.IMAGE == attr.cellType()) + { + ClientAnchor anchor = new XSSFClientAnchor(0, 0, 0, 0, (short) cell.getColumnIndex(), cell.getRow().getRowNum(), (short) (cell.getColumnIndex() + 1), + cell.getRow().getRowNum() + 1); + String imagePath = Convert.toStr(value); + if (Validator.isNotEmpty(imagePath)) + { + byte[] data = ImageUtils.getImage(imagePath); + getDrawingPatriarch(cell.getSheet()).createPicture(anchor, + cell.getSheet().getWorkbook().addPicture(data, getImageType(data))); + } + } + } + + /** + * 获取画布 + */ + public static Drawing getDrawingPatriarch(Sheet sheet) + { + if (sheet.getDrawingPatriarch() == null) + { + sheet.createDrawingPatriarch(); + } + return sheet.getDrawingPatriarch(); + } + + /** + * 获取图片类型,设置图片插入类型 + */ + public int getImageType(byte[] value) + { + String type = FileTypeUtils.getFileExtendName(value); + if ("JPG".equalsIgnoreCase(type)) + { + return Workbook.PICTURE_TYPE_JPEG; + } + else if ("PNG".equalsIgnoreCase(type)) + { + return Workbook.PICTURE_TYPE_PNG; + } + return Workbook.PICTURE_TYPE_JPEG; + } + + /** + * 创建表格样式 + */ + public void setDataValidation(Excel attr, Row row, int column) + { + if (attr.name().indexOf("注:") >= 0) + { + sheet.setColumnWidth(column, 6000); + } + else + { + // 设置列宽 + sheet.setColumnWidth(column, (int) ((attr.width() + 0.72) * 256)); + } + // 如果设置了提示信息则鼠标放上去提示. + if (Validator.isNotEmpty(attr.prompt())) + { + // 这里默认设了2-101列提示. + setXSSFPrompt(sheet, "", attr.prompt(), 1, 100, column, column); + } + // 如果设置了combo属性则本列只能选择不能输入 + if (attr.combo().length > 0) + { + // 这里默认设了2-101列只能选择不能输入. + setXSSFValidation(sheet, attr.combo(), 1, 100, column, column); + } + } + + /** + * 添加单元格 + */ + public Cell addCell(Excel attr, Row row, T vo, Field field, int column) + { + Cell cell = null; + try + { + // 设置行高 + row.setHeight(maxHeight); + // 根据Excel中设置情况决定是否导出,有些情况需要保持为空,希望用户填写这一列. + if (attr.isExport()) + { + // 创建cell + cell = row.createCell(column); + int align = attr.align().value(); + cell.setCellStyle(styles.get("data" + (align >= 1 && align <= 3 ? align : ""))); + + // 用于读取对象中的属性 + Object value = getTargetValue(vo, field, attr); + String dateFormat = attr.dateFormat(); + String readConverterExp = attr.readConverterExp(); + String separator = attr.separator(); + String dictType = attr.dictType(); + if (Validator.isNotEmpty(dateFormat) && Validator.isNotNull(value)) + { + cell.setCellValue(DateUtils.parseDateToStr(dateFormat, (Date) value)); + } + else if (Validator.isNotEmpty(readConverterExp) && Validator.isNotNull(value)) + { + cell.setCellValue(convertByExp(Convert.toStr(value), readConverterExp, separator)); + } + else if (Validator.isNotEmpty(dictType) && Validator.isNotNull(value)) + { + cell.setCellValue(convertDictByExp(Convert.toStr(value), dictType, separator)); + } + else if (value instanceof BigDecimal && -1 != attr.scale()) + { + cell.setCellValue((((BigDecimal) value).setScale(attr.scale(), attr.roundingMode())).toString()); + } + else + { + // 设置列类型 + setCellVo(value, attr, cell); + } + addStatisticsData(column, Convert.toStr(value), attr); + } + } + catch (Exception e) + { + log.error("导出Excel失败{}", e); + } + return cell; + } + + /** + * 设置 POI XSSFSheet 单元格提示 + * + * @param sheet 表单 + * @param promptTitle 提示标题 + * @param promptContent 提示内容 + * @param firstRow 开始行 + * @param endRow 结束行 + * @param firstCol 开始列 + * @param endCol 结束列 + */ + public void setXSSFPrompt(Sheet sheet, String promptTitle, String promptContent, int firstRow, int endRow, + int firstCol, int endCol) + { + DataValidationHelper helper = sheet.getDataValidationHelper(); + DataValidationConstraint constraint = helper.createCustomConstraint("DD1"); + CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol); + DataValidation dataValidation = helper.createValidation(constraint, regions); + dataValidation.createPromptBox(promptTitle, promptContent); + dataValidation.setShowPromptBox(true); + sheet.addValidationData(dataValidation); + } + + /** + * 设置某些列的值只能输入预制的数据,显示下拉框. + * + * @param sheet 要设置的sheet. + * @param textlist 下拉框显示的内容 + * @param firstRow 开始行 + * @param endRow 结束行 + * @param firstCol 开始列 + * @param endCol 结束列 + * @return 设置好的sheet. + */ + public void setXSSFValidation(Sheet sheet, String[] textlist, int firstRow, int endRow, int firstCol, int endCol) + { + DataValidationHelper helper = sheet.getDataValidationHelper(); + // 加载下拉列表内容 + DataValidationConstraint constraint = helper.createExplicitListConstraint(textlist); + // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列 + CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol); + // 数据有效性对象 + DataValidation dataValidation = helper.createValidation(constraint, regions); + // 处理Excel兼容性问题 + if (dataValidation instanceof XSSFDataValidation) + { + dataValidation.setSuppressDropDownArrow(true); + dataValidation.setShowErrorBox(true); + } + else + { + dataValidation.setSuppressDropDownArrow(false); + } + + sheet.addValidationData(dataValidation); + } + + /** + * 解析导出值 0=男,1=女,2=未知 + * + * @param propertyValue 参数值 + * @param converterExp 翻译注解 + * @param separator 分隔符 + * @return 解析后值 + */ + public static String convertByExp(String propertyValue, String converterExp, String separator) + { + StringBuilder propertyString = new StringBuilder(); + String[] convertSource = converterExp.split(","); + for (String item : convertSource) + { + String[] itemArray = item.split("="); + if (StrUtil.containsAny(separator, propertyValue)) + { + for (String value : propertyValue.split(separator)) + { + if (itemArray[0].equals(value)) + { + propertyString.append(itemArray[1] + separator); + break; + } + } + } + else + { + if (itemArray[0].equals(propertyValue)) + { + return itemArray[1]; + } + } + } + return StrUtil.strip(propertyString.toString(), null,separator); + } + + /** + * 反向解析值 男=0,女=1,未知=2 + * + * @param propertyValue 参数值 + * @param converterExp 翻译注解 + * @param separator 分隔符 + * @return 解析后值 + */ + public static String reverseByExp(String propertyValue, String converterExp, String separator) + { + StringBuilder propertyString = new StringBuilder(); + String[] convertSource = converterExp.split(","); + for (String item : convertSource) + { + String[] itemArray = item.split("="); + if (StrUtil.containsAny(separator, propertyValue)) + { + for (String value : propertyValue.split(separator)) + { + if (itemArray[1].equals(value)) + { + propertyString.append(itemArray[0] + separator); + break; + } + } + } + else + { + if (itemArray[1].equals(propertyValue)) + { + return itemArray[0]; + } + } + } + return StrUtil.strip(propertyString.toString(), null,separator); + } + + /** + * 解析字典值 + * + * @param dictValue 字典值 + * @param dictType 字典类型 + * @param separator 分隔符 + * @return 字典标签 + */ + public static String convertDictByExp(String dictValue, String dictType, String separator) + { + return DictUtils.getDictLabel(dictType, dictValue, separator); + } + + /** + * 反向解析值字典值 + * + * @param dictLabel 字典标签 + * @param dictType 字典类型 + * @param separator 分隔符 + * @return 字典值 + */ + public static String reverseDictByExp(String dictLabel, String dictType, String separator) + { + return DictUtils.getDictValue(dictType, dictLabel, separator); + } + + /** + * 合计统计信息 + */ + private void addStatisticsData(Integer index, String text, Excel entity) + { + if (entity != null && entity.isStatistics()) + { + Double temp = 0D; + if (!statistics.containsKey(index)) + { + statistics.put(index, temp); + } + try + { + temp = Double.valueOf(text); + } + catch (NumberFormatException e) + { + } + statistics.put(index, statistics.get(index) + temp); + } + } + + /** + * 创建统计行 + */ + public void addStatisticsRow() + { + if (statistics.size() > 0) + { + Cell cell = null; + Row row = sheet.createRow(sheet.getLastRowNum() + 1); + Set keys = statistics.keySet(); + cell = row.createCell(0); + cell.setCellStyle(styles.get("total")); + cell.setCellValue("合计"); + + for (Integer key : keys) + { + cell = row.createCell(key); + cell.setCellStyle(styles.get("total")); + cell.setCellValue(DOUBLE_FORMAT.format(statistics.get(key))); + } + statistics.clear(); + } + } + + /** + * 编码文件名 + */ + public String encodingFilename(String filename) + { + filename = UUID.randomUUID().toString() + "_" + filename + ".xlsx"; + return filename; + } + + /** + * 获取下载路径 + * + * @param filename 文件名称 + */ + public String getAbsoluteFile(String filename) + { + String downloadPath = BsConfig.getDownloadPath() + filename; + File desc = new File(downloadPath); + if (!desc.getParentFile().exists()) + { + desc.getParentFile().mkdirs(); + } + return downloadPath; + } + + /** + * 获取bean中的属性值 + * + * @param vo 实体对象 + * @param field 字段 + * @param excel 注解 + * @return 最终的属性值 + * @throws Exception + */ + private Object getTargetValue(T vo, Field field, Excel excel) throws Exception + { + Object o = field.get(vo); + if (Validator.isNotEmpty(excel.targetAttr())) + { + String target = excel.targetAttr(); + if (target.contains(".")) + { + String[] targets = target.split("[.]"); + for (String name : targets) + { + o = getValue(o, name); + } + } + else + { + o = getValue(o, target); + } + } + return o; + } + + /** + * 以类的属性的get方法方法形式获取值 + * + * @param o + * @param name + * @return value + * @throws Exception + */ + private Object getValue(Object o, String name) throws Exception + { + if (Validator.isNotNull(o) && Validator.isNotEmpty(name)) + { + Class clazz = o.getClass(); + Field field = clazz.getDeclaredField(name); + field.setAccessible(true); + o = field.get(o); + } + return o; + } + + /** + * 得到所有定义字段 + */ + private void createExcelField() + { + this.fields = new ArrayList(); + List tempFields = new ArrayList<>(); + tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields())); + tempFields.addAll(Arrays.asList(clazz.getDeclaredFields())); + for (Field field : tempFields) + { + // 单注解 + if (field.isAnnotationPresent(Excel.class)) + { + putToField(field, field.getAnnotation(Excel.class)); + } + + // 多注解 + if (field.isAnnotationPresent(Excels.class)) + { + Excels attrs = field.getAnnotation(Excels.class); + Excel[] excels = attrs.value(); + for (Excel excel : excels) + { + putToField(field, excel); + } + } + } + this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList()); + this.maxHeight = getRowHeight(); + } + + /** + * 根据注解获取最大行高 + */ + public short getRowHeight() + { + double maxHeight = 0; + for (Object[] os : this.fields) + { + Excel excel = (Excel) os[1]; + maxHeight = maxHeight > excel.height() ? maxHeight : excel.height(); + } + return (short) (maxHeight * 20); + } + + /** + * 放到字段集合中 + */ + private void putToField(Field field, Excel attr) + { + if (attr != null && (attr.type() == Type.ALL || attr.type() == type)) + { + this.fields.add(new Object[] { field, attr }); + } + } + + /** + * 创建一个工作簿 + */ + public void createWorkbook() + { + this.wb = new SXSSFWorkbook(500); + } + + /** + * 创建工作表 + * + * @param sheetNo sheet数量 + * @param index 序号 + */ + public void createSheet(double sheetNo, int index) + { + this.sheet = wb.createSheet(); + this.styles = createStyles(wb); + // 设置工作表的名称. + if (sheetNo == 0) + { + wb.setSheetName(index, sheetName); + } + else + { + wb.setSheetName(index, sheetName + index); + } + } + + /** + * 获取单元格值 + * + * @param row 获取的行 + * @param column 获取单元格列号 + * @return 单元格值 + */ + public Object getCellValue(Row row, int column) + { + if (row == null) + { + return row; + } + Object val = ""; + try + { + Cell cell = row.getCell(column); + if (Validator.isNotNull(cell)) + { + if (cell.getCellType() == CellType.NUMERIC || cell.getCellType() == CellType.FORMULA) + { + val = cell.getNumericCellValue(); + if (DateUtil.isCellDateFormatted(cell)) + { + val = DateUtil.getJavaDate((Double) val); // POI Excel 日期格式转换 + } + else + { + if ((Double) val % 1 != 0) + { + val = new BigDecimal(val.toString()); + } + else + { + val = new DecimalFormat("0").format(val); + } + } + } + else if (cell.getCellType() == CellType.STRING) + { + val = cell.getStringCellValue(); + } + else if (cell.getCellType() == CellType.BOOLEAN) + { + val = cell.getBooleanCellValue(); + } + else if (cell.getCellType() == CellType.ERROR) + { + val = cell.getErrorCellValue(); + } + + } + } + catch (Exception e) + { + return val; + } + return val; + } +} diff --git a/bashi-common/src/main/java/com/bashi/common/utils/reflect/ReflectUtils.java b/bashi-common/src/main/java/com/bashi/common/utils/reflect/ReflectUtils.java new file mode 100644 index 0000000..c686f8f --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/utils/reflect/ReflectUtils.java @@ -0,0 +1,54 @@ +package com.bashi.common.utils.reflect; + +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; + +import java.lang.reflect.Method; +import java.util.List; + +/** + * 反射工具类. 提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数. + * + * @author Lion Li + */ +@SuppressWarnings("rawtypes") +public class ReflectUtils extends ReflectUtil { + + private static final String SETTER_PREFIX = "set"; + + private static final String GETTER_PREFIX = "get"; + + /** + * 调用Getter方法. + * 支持多级,如:对象名.对象名.方法 + */ + @SuppressWarnings("unchecked") + public static E invokeGetter(Object obj, String propertyName) { + Object object = obj; + for (String name : StrUtil.split(propertyName, ".")) { + String getterMethodName = GETTER_PREFIX + StrUtil.upperFirst(name); + object = invoke(object, getterMethodName); + } + return (E) object; + } + + /** + * 调用Setter方法, 仅匹配方法名。 + * 支持多级,如:对象名.对象名.方法 + */ + public static void invokeSetter(Object obj, String propertyName, E value) { + Object object = obj; + List names = StrUtil.split(propertyName, "."); + for (int i = 0; i < names.size(); i++) { + if (i < names.size() - 1) { + String getterMethodName = GETTER_PREFIX + StrUtil.upperFirst(names.get(i)); + object = invoke(object, getterMethodName); + } else { + String setterMethodName = SETTER_PREFIX + StrUtil.upperFirst(names.get(i)); + Method method = getMethodByName(object.getClass(), setterMethodName); + invoke(object, method, value); + } + } + } + +} diff --git a/bashi-common/src/main/java/com/bashi/common/utils/spring/SpringUtils.java b/bashi-common/src/main/java/com/bashi/common/utils/spring/SpringUtils.java new file mode 100644 index 0000000..9c0cc37 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/utils/spring/SpringUtils.java @@ -0,0 +1,65 @@ +package com.bashi.common.utils.spring; + +import cn.hutool.extra.spring.SpringUtil; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.stereotype.Component; + +/** + * spring工具类 + * + * @author Lion Li + */ +@Component +public final class SpringUtils extends SpringUtil { + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + * + * @param name + * @return boolean + */ + public static boolean containsBean(String name) { + return getBeanFactory().containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 + * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + * + * @param name + * @return boolean + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().isSingleton(name); + } + + /** + * @param name + * @return Class 注册对象的类型 + */ + public static Class getType(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + * + * @param name + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getAliases(name); + } + + /** + * 获取aop代理对象 + * + * @param invoker + * @return + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) { + return (T) AopContext.currentProxy(); + } + +} diff --git a/bashi-common/src/main/java/com/bashi/common/utils/sql/SqlUtil.java b/bashi-common/src/main/java/com/bashi/common/utils/sql/SqlUtil.java new file mode 100644 index 0000000..52603a1 --- /dev/null +++ b/bashi-common/src/main/java/com/bashi/common/utils/sql/SqlUtil.java @@ -0,0 +1,37 @@ +package com.bashi.common.utils.sql; + +import cn.hutool.core.lang.Validator; +import com.bashi.common.exception.BaseException; + +/** + * sql操作工具类 + * + * @author duteliang + */ +public class SqlUtil +{ + /** + * 仅支持字母、数字、下划线、空格、逗号、小数点(支持多个字段排序) + */ + public static String SQL_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+"; + + /** + * 检查字符,防止注入绕过 + */ + public static String escapeOrderBySql(String value) + { + if (Validator.isNotEmpty(value) && !isValidOrderBySql(value)) + { + throw new BaseException("参数不符合规范,不能进行查询"); + } + return value; + } + + /** + * 验证 order by 语法是否符合规范 + */ + public static boolean isValidOrderBySql(String value) + { + return value.matches(SQL_PATTERN); + } +} diff --git a/bashi-dk/pom.xml b/bashi-dk/pom.xml new file mode 100644 index 0000000..f5a74c8 --- /dev/null +++ b/bashi-dk/pom.xml @@ -0,0 +1,51 @@ + + + + bashi + com.bashi + 2.4.0 + + 4.0.0 + + bashi-dk + + + 8 + 8 + UTF-8 + + + + + + com.bashi + bashi-common + + + com.bashi + bashi-framework + + + org.dromara.x-file-storage + x-file-storage-spring + 2.0.0 + + + + com.aliyun.oss + aliyun-sdk-oss + 3.16.1 + + + com.qcloud + cos_api + 5.6.137 + + + org.freemarker + freemarker + + + diff --git a/bashi-dk/src/main/java/com/bashi/dk/controller/BorrowStatusController.java b/bashi-dk/src/main/java/com/bashi/dk/controller/BorrowStatusController.java new file mode 100644 index 0000000..7c9289d --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/controller/BorrowStatusController.java @@ -0,0 +1,103 @@ +package com.bashi.dk.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.bashi.common.annotation.Log; +import com.bashi.common.annotation.RepeatSubmit; +import com.bashi.common.com.Condition; +import com.bashi.common.com.PageParams; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.enums.BusinessType; +import com.bashi.common.utils.PageUtils; +import com.bashi.dk.domain.BorrowStatus; +import com.bashi.dk.enums.BankTypeEnums; +import com.bashi.dk.service.BorrowStatusService; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@RestController +@RequestMapping("/dk/BorrowStatus") +public class BorrowStatusController extends BaseController { + + private final BorrowStatusService borrowStatusService; + + /** + * 查询借款状态列表 + */ + @PreAuthorize("@ss.hasPermi('dk:BorrowStatus:list')") + @GetMapping("/list") + public TableDataInfo list(PageParams pageParams, @Validated BorrowStatus bo) { + IPage page = borrowStatusService.page(Condition.getPage(pageParams), Wrappers.query(bo)); + return PageUtils.buildDataInfo(page); + } + + @GetMapping("/bankType") + public AjaxResult> bankType(){ + List list = Arrays.stream(BankTypeEnums.values()).map(BankTypeEnums::getName).collect(Collectors.toList()); + return AjaxResult.success(list); + } + + @GetMapping("/all") + public AjaxResult> all(){ + List list = borrowStatusService.list(); + return AjaxResult.success(list); + } + + /** + * 获取借款状态详细信息 + */ + @PreAuthorize("@ss.hasPermi('dk:BorrowStatus:query')") + @GetMapping("/{id}") + public AjaxResult getInfo(@NotNull(message = "主键不能为空") + @PathVariable("id") Long id) { + return AjaxResult.success(borrowStatusService.getById(id)); + } + + /** + * 新增借款状态 + */ + @PreAuthorize("@ss.hasPermi('dk:BorrowStatus:add')") + @Log(title = "借款状态", businessType = BusinessType.INSERT) + @RepeatSubmit + @PostMapping() + public AjaxResult add(@Validated @RequestBody BorrowStatus bo) { + return toAjax(borrowStatusService.save(bo) ? 1 : 0); + } + + /** + * 修改借款状态 + */ + @PreAuthorize("@ss.hasPermi('dk:BorrowStatus:edit')") + @Log(title = "借款状态", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PutMapping() + public AjaxResult edit(@Validated @RequestBody BorrowStatus bo) { + return toAjax(borrowStatusService.updateById(bo) ? 1 : 0); + } + + /** + * 删除借款状态 + */ + @PreAuthorize("@ss.hasPermi('dk:BorrowStatus:remove')") + @Log(title = "借款状态" , businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@NotEmpty(message = "主键不能为空") + @PathVariable String ids) { + List idList = Stream.of(ids.split(",")).collect(Collectors.toList()); + return toAjax(borrowStatusService.removeByIds(idList) ? 1 : 0); + } +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/controller/DkAgreementSettingController.java b/bashi-dk/src/main/java/com/bashi/dk/controller/DkAgreementSettingController.java new file mode 100644 index 0000000..faca27e --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/controller/DkAgreementSettingController.java @@ -0,0 +1,43 @@ +package com.bashi.dk.controller; + +import com.bashi.common.annotation.Log; +import com.bashi.common.annotation.RepeatSubmit; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.enums.BusinessType; +import com.bashi.dk.domain.AgreementSetting; +import com.bashi.dk.service.AgreementSettingService; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@RestController +@RequestMapping("/dk/AgreementSetting") +public class DkAgreementSettingController extends BaseController { + + private final AgreementSettingService agreementSettingService; + + /** + * 获取协议设置详细信息 + */ + @PreAuthorize("@ss.hasPermi('dk:AgreementSetting:query')") + @GetMapping("/info") + public AjaxResult getInfo() { + return AjaxResult.success(agreementSettingService.getAgreementSetting()); + } + + /** + * 修改协议设置 + */ + @PreAuthorize("@ss.hasPermi('dk:AgreementSetting:edit')") + @Log(title = "协议设置", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PutMapping() + public AjaxResult edit(@Validated @RequestBody AgreementSetting bo) { + return toAjax(agreementSettingService.updateById(bo) ? 1 : 0); + } + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/controller/DkBorrowController.java b/bashi-dk/src/main/java/com/bashi/dk/controller/DkBorrowController.java new file mode 100644 index 0000000..e566849 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/controller/DkBorrowController.java @@ -0,0 +1,124 @@ +package com.bashi.dk.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.bashi.common.annotation.Log; +import com.bashi.common.annotation.RepeatSubmit; +import com.bashi.common.com.Condition; +import com.bashi.common.com.PageParams; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.enums.BusinessType; +import com.bashi.common.utils.PageUtils; +import com.bashi.dk.domain.Borrow; +import com.bashi.dk.dto.admin.req.BorrowUpdateStatusReq; +import com.bashi.dk.dto.admin.resp.BorrowResp; +import com.bashi.dk.service.BorrowService; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@RestController +@RequestMapping("/dk/borrow") +public class DkBorrowController extends BaseController { + + private final BorrowService borrowService; + + /** + * 查询借款计划列表 + */ + @PreAuthorize("@ss.hasPermi('dk:borrow:list')") + @GetMapping("/list") + public TableDataInfo list(PageParams pageParams, @Validated Borrow bo) { + IPage page = borrowService.pageAdmin(pageParams, bo); + return PageUtils.buildDataInfo(page); + } + + + /** + * 获取借款计划详细信息 + */ + @PreAuthorize("@ss.hasPermi('dk:borrow:query')") + @GetMapping("/{id}") + public AjaxResult getInfo(@NotNull(message = "主键不能为空") + @PathVariable("id") Long id) { + return AjaxResult.success(borrowService.getById(id)); + } + + /** + * 新增借款计划 + */ + @PreAuthorize("@ss.hasPermi('dk:borrow:add')") + @Log(title = "借款计划", businessType = BusinessType.INSERT) + @RepeatSubmit + @PostMapping() + public AjaxResult add(@Validated @RequestBody Borrow bo) { + return toAjax(borrowService.save(bo) ? 1 : 0); + } + + /** + * 修改借款计划 + */ + @PreAuthorize("@ss.hasPermi('dk:borrow:edit')") + @Log(title = "借款计划", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PutMapping() + public AjaxResult edit(@Validated @RequestBody Borrow bo) { + return toAjax(borrowService.updateById(bo) ? 1 : 0); + } + + @Log(title = "修改接口银行卡", businessType = BusinessType.UPDATE) + @PostMapping("updateBank") + public AjaxResult updateBank(@RequestBody Borrow bo) { + return toAjax(borrowService.updateBank(bo)); + } + + @PreAuthorize("@ss.hasPermi('dk:borrow:edit')") + @Log(title = "修改借款", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PostMapping("/updateLoan") + public AjaxResult updateLoan(@Validated @RequestBody Borrow bo) { + return toAjax(borrowService.updateLoan(bo)); + } + + + @PreAuthorize("@ss.hasPermi('dk:borrow:edit')") + @Log(title = "修改借款状态", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PostMapping("/updateStatus") + public AjaxResult updateStatus(@Validated @RequestBody BorrowUpdateStatusReq bo) { + return toAjax(borrowService.updateStatus(bo)); + } + + + /** + * 删除借款计划 + */ + @PreAuthorize("@ss.hasPermi('dk:borrow:remove')") + @Log(title = "借款计划" , businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@NotEmpty(message = "主键不能为空") + @PathVariable String ids) { + List idList = Stream.of(ids.split(",")).collect(Collectors.toList()); + return toAjax(borrowService.removeByIds(idList) ? 1 : 0); + } + + @GetMapping("/getContract") + public AjaxResult getContract(String tradeNo){ + String contract = borrowService.getContract(tradeNo); + AjaxResult success = AjaxResult.success(); + success.setData(contract); + return success; + } +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/controller/DkCustomerController.java b/bashi-dk/src/main/java/com/bashi/dk/controller/DkCustomerController.java new file mode 100644 index 0000000..b4cc42d --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/controller/DkCustomerController.java @@ -0,0 +1,88 @@ +package com.bashi.dk.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.bashi.common.annotation.Log; +import com.bashi.common.annotation.RepeatSubmit; +import com.bashi.common.com.PageParams; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.domain.entity.Customer; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.enums.BusinessType; +import com.bashi.common.utils.PageUtils; +import com.bashi.common.utils.poi.ExcelUtil; +import com.bashi.dk.dto.admin.req.UpdatePwdCustomerReq; +import com.bashi.dk.dto.admin.resp.CustomerAdminResp; +import com.bashi.dk.dto.admin.resp.CustomerExportVo; +import com.bashi.dk.mapper.CustomerMapper; +import com.bashi.dk.service.CustomerService; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@RestController +@RequestMapping("/dk/dkCustomer") +public class DkCustomerController extends BaseController { + + private final CustomerService customerService; + @Resource + private CustomerMapper customerMapper; + + @PreAuthorize("@ss.hasPermi('dk:dkCustomer:list')") + @GetMapping("/list") + public TableDataInfo list(PageParams pageParams, @Validated CustomerAdminResp bo) { + IPage page = customerService.pageAdmin(pageParams, bo); + return PageUtils.buildDataInfo(page); + } + + @ApiOperation("导出客户列表") + @PreAuthorize("@ss.hasPermi('dk:dkCustomer:export')") + @Log(title = "客户", businessType = BusinessType.EXPORT) + @GetMapping("/export") + public AjaxResult export(@Validated CustomerAdminResp bo) { + List list = customerMapper.exportAdmin(bo); + ExcelUtil util = new ExcelUtil<>(CustomerExportVo.class); + return util.exportExcel(list, "客户"); + } + + @PreAuthorize("@ss.hasPermi('dk:dkCustomer:query')") + @GetMapping("/{id}") + public AjaxResult getInfo(@NotNull(message = "主键不能为空") + @PathVariable("id") Long id) { + return AjaxResult.success(customerService.getById(id)); + } + + + @PreAuthorize("@ss.hasPermi('dk:dkCustomer:edit')") + @Log(title = "客户", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PutMapping() + public AjaxResult edit(@Validated @RequestBody Customer bo) { + return toAjax(customerService.updateById(bo) ? 1 : 0); + } + + @DeleteMapping("/{ids}") + public AjaxResult remove(@NotEmpty(message = "主键不能为空") + @PathVariable String ids) { + List idList = Stream.of(ids.split(",")).collect(Collectors.toList()); + return toAjax(customerService.removeByIds(idList) ? 1 : 0); + } + + + @Log(title = "修改密码" , businessType = BusinessType.DELETE) + @PostMapping("/resetPwd") + public AjaxResult resetPwd(@RequestBody UpdatePwdCustomerReq customer) { + return toAjax(customerService.updatePwd(customer.getCustomerId(),customer.getPassword())); + } +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/controller/DkCustomerInfoController.java b/bashi-dk/src/main/java/com/bashi/dk/controller/DkCustomerInfoController.java new file mode 100644 index 0000000..bd3a18a --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/controller/DkCustomerInfoController.java @@ -0,0 +1,64 @@ +package com.bashi.dk.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.bashi.common.annotation.Log; +import com.bashi.common.annotation.RepeatSubmit; +import com.bashi.common.com.Condition; +import com.bashi.common.com.PageParams; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.enums.BusinessType; +import com.bashi.common.utils.PageUtils; +import com.bashi.dk.domain.CustomerInfo; +import com.bashi.dk.service.CustomerInfoService; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.constraints.NotNull; + +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@RestController +@RequestMapping("/dk/dkCustomerInfo") +public class DkCustomerInfoController extends BaseController { + + private final CustomerInfoService customerInfoService; + + @PreAuthorize("@ss.hasPermi('dk:dkCustomerInfo:list')") + @GetMapping("/list") + public TableDataInfo list(PageParams pageParams, @Validated CustomerInfo bo) { + IPage page = customerInfoService.page(Condition.getPage(pageParams), Wrappers.query(bo)); + return PageUtils.buildDataInfo(page); + } + + @GetMapping("/getInfoByCustomerId") + public AjaxResult getInfoByCustomer(Long customerId) { + return AjaxResult.success(customerInfoService.getByCustomerId(customerId)); + } + + @PostMapping("/updateAllowSignature") + public AjaxResult updateAllowSignature(@RequestBody CustomerInfo bo) { + customerInfoService.updateAllowSignature(bo); + return AjaxResult.success(); + } + + @PreAuthorize("@ss.hasPermi('dk:dkCustomerInfo:query')") + @GetMapping("/{id}") + public AjaxResult getInfo(@NotNull(message = "主键不能为空") + @PathVariable("id") Long id) { + return AjaxResult.success(customerInfoService.getById(id)); + } + + @PreAuthorize("@ss.hasPermi('dk:dkCustomerInfo:edit')") + @Log(title = "客户资料", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PutMapping() + public AjaxResult edit(@Validated @RequestBody CustomerInfo bo) { + return toAjax(customerInfoService.updateById(bo) ? 1 : 0); + } + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/controller/DkHomeSettingController.java b/bashi-dk/src/main/java/com/bashi/dk/controller/DkHomeSettingController.java new file mode 100644 index 0000000..e3a1df0 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/controller/DkHomeSettingController.java @@ -0,0 +1,51 @@ +package com.bashi.dk.controller; + +import com.bashi.common.annotation.Log; +import com.bashi.common.annotation.RepeatSubmit; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.enums.BusinessType; +import com.bashi.dk.domain.HomeSetting; +import com.bashi.dk.service.HomeSettingService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + * 常规设置Controller + * + * @author duteliang + * @date 2023-11-29 + */ +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@RestController +@RequestMapping("/dk/HomeSetting") +public class DkHomeSettingController extends BaseController { + + private final HomeSettingService homeSettingService; + + + /** + * 获取常规设置详细信息 + */ + @PreAuthorize("@ss.hasPermi('dk:HomeSetting:query')") + @GetMapping("/info") + public AjaxResult getInfo() { + return AjaxResult.success(homeSettingService.getHomeSetting()); + } + + /** + * 修改常规设置 + */ + @PreAuthorize("@ss.hasPermi('dk:HomeSetting:edit')") + @Log(title = "常规设置", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PutMapping() + public AjaxResult edit(@Validated @RequestBody HomeSetting bo) { + return toAjax(homeSettingService.updateById(bo) ? 1 : 0); + } +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/controller/DkLoansSettingController.java b/bashi-dk/src/main/java/com/bashi/dk/controller/DkLoansSettingController.java new file mode 100644 index 0000000..83c5434 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/controller/DkLoansSettingController.java @@ -0,0 +1,50 @@ +package com.bashi.dk.controller; + +import com.bashi.common.annotation.Log; +import com.bashi.common.annotation.RepeatSubmit; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.enums.BusinessType; +import com.bashi.dk.domain.LoansSetting; +import com.bashi.dk.service.LoansSettingService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + * 贷款设置Controller + * + * @author duteliang + * @date 2023-11-29 + */ +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@RestController +@RequestMapping("/dk/LoansSetting") +public class DkLoansSettingController extends BaseController { + + private final LoansSettingService loansSettingService; + + /** + * 获取贷款设置详细信息 + */ + @PreAuthorize("@ss.hasPermi('dk:LoansSetting:query')") + @GetMapping("/info") + public AjaxResult getInfo() { + return AjaxResult.success(loansSettingService.getLoansSetting()); + } + + /** + * 修改贷款设置 + */ + @PreAuthorize("@ss.hasPermi('dk:LoansSetting:edit')") + @Log(title = "贷款设置", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PutMapping() + public AjaxResult edit(@Validated @RequestBody LoansSetting bo) { + return toAjax(loansSettingService.updateById(bo) ? 1 : 0); + } +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/controller/app/AppBorrowController.java b/bashi-dk/src/main/java/com/bashi/dk/controller/app/AppBorrowController.java new file mode 100644 index 0000000..e5d267d --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/controller/app/AppBorrowController.java @@ -0,0 +1,88 @@ +package com.bashi.dk.controller.app; + + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.bashi.common.com.Condition; +import com.bashi.common.com.PageParams; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.utils.BeanConvertUtil; +import com.bashi.common.utils.PageUtils; +import com.bashi.common.utils.SecurityUtils; +import com.bashi.dk.domain.Borrow; +import com.bashi.dk.dto.app.req.BorrowStartReq; +import com.bashi.dk.dto.app.resp.BorrowInfo; +import com.bashi.dk.dto.app.resp.LoanProcessResp; +import com.bashi.dk.service.BorrowService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/app/borrow") +@Api(value = "贷款相关的接口", tags = {"贷款相关的接口"}) +public class AppBorrowController { + + @Autowired + private BorrowService borrowService; + + @PostMapping("/start") + @ApiOperation(value = "发起贷款") + public AjaxResult start(@RequestBody BorrowStartReq req){ + req.setCustomerId(SecurityUtils.getLoginUser().getCustomer().getId()); + Borrow borrow = borrowService.borrow(req); + return AjaxResult.success(borrow); + } + + @GetMapping("/withdraw") + @ApiOperation(value = "提现") + public AjaxResult withdraw(@ApiParam("提现金额") Double withdrawAmount){ + Long customerId = SecurityUtils.getLoginUser().getCustomer().getId(); + borrowService.withdraw(withdrawAmount,customerId); + return AjaxResult.success(); + } + + + @GetMapping("/getStepBorrow") + @ApiOperation(value = "获取贷款进度") + public AjaxResult getStepBorrow(){ + Long customerId = SecurityUtils.getLoginUser().getCustomer().getId(); + LoanProcessResp stepBorrow = borrowService.getStepBorrow(customerId); + return AjaxResult.success(stepBorrow); + } + + + @GetMapping("/info") + @ApiOperation(value = "查看贷款详情") + public AjaxResult info(String tradeNo){ + Borrow borrow = borrowService.getByTradeNo(tradeNo); + BorrowInfo borrowInfo = BeanConvertUtil.convertTo(borrow, BorrowInfo::new); + LoanProcessResp stepBorrow = borrowService.parseStepBorrow(borrow); + borrowInfo.setLoanProcessResp(stepBorrow); + return AjaxResult.success(borrowInfo); + } + + @GetMapping("/page") + @ApiOperation(value = "分页查询贷款我的贷款") + public TableDataInfo page(PageParams pageParams){ + Long customerId = SecurityUtils.getLoginUser().getCustomer().getId(); + LambdaQueryWrapper query = Wrappers.lambdaQuery(Borrow.class) + .eq(Borrow::getCustomerId,customerId) + .orderByDesc(Borrow::getCreateTime); + IPage page = borrowService.page(Condition.getPage(pageParams), query); + return PageUtils.buildDataInfo(page); + } + + + @GetMapping("/getContract") + public AjaxResult getContract(String tradeNo){ + String contract = borrowService.getContract(tradeNo); + AjaxResult success = AjaxResult.success(); + success.setData(contract); + return success; + } +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/controller/app/AppCustomerController.java b/bashi-dk/src/main/java/com/bashi/dk/controller/app/AppCustomerController.java new file mode 100644 index 0000000..acdb3c6 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/controller/app/AppCustomerController.java @@ -0,0 +1,50 @@ +package com.bashi.dk.controller.app; + +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.domain.entity.Customer; +import com.bashi.common.utils.SecurityUtils; +import com.bashi.dk.domain.CustomerInfo; +import com.bashi.dk.service.CustomerInfoService; +import com.bashi.dk.service.CustomerService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/app/customer") +@Api(value = "客户接口", tags = {"客户接口"}) +public class AppCustomerController { + + @Autowired + private CustomerInfoService customerInfoService; + @Autowired + private CustomerService customerService; + + @GetMapping("/info") + @ApiOperation(value = "客户信息") + public AjaxResult info(){ + Customer customer = SecurityUtils.getLoginUser().getCustomer(); + customer = customerService.getById(customer.getId()); + return AjaxResult.success(customer); + } + + @GetMapping("/card/info") + @ApiOperation(value = "客户资料信息") + public AjaxResult cardInfo(){ + Customer customer = SecurityUtils.getLoginUser().getCustomer(); + CustomerInfo customerInfo = customerInfoService.getByCustomerId(customer.getId()); + return AjaxResult.success(customerInfo); + } + + @PostMapping("/updateCustomerCard") + @ApiOperation(value = "修改客户资料信息") + public AjaxResult updateCustomerCard(@RequestBody CustomerInfo customerInfo){ + Customer customer = SecurityUtils.getLoginUser().getCustomer(); + customerInfo.setCustomerId(customer.getId()); + customerInfoService.updateCustomerInfo(customerInfo); + return AjaxResult.success(); + } + + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/controller/app/AppCustomerOpenController.java b/bashi-dk/src/main/java/com/bashi/dk/controller/app/AppCustomerOpenController.java new file mode 100644 index 0000000..f32081a --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/controller/app/AppCustomerOpenController.java @@ -0,0 +1,89 @@ +package com.bashi.dk.controller.app; + +import cn.hutool.core.lang.UUID; +import cn.hutool.core.util.RandomUtil; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.domain.entity.Customer; +import com.bashi.common.core.redis.RedisCache; +import com.bashi.common.exception.CustomException; +import com.bashi.dk.dto.app.req.CustomerRegisterReq; +import com.bashi.dk.dto.app.req.UpdatePwdOpenReq; +import com.bashi.dk.service.CustomerService; +import com.bashi.framework.constant.CodeType; +import com.bashi.framework.web.service.CodeService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@RequestMapping("/customer/open") +@RestController +@Api(value = "用户开放接口", tags = {"用户开放接口"}) +public class AppCustomerOpenController { + @Autowired + private CodeService codeService; + @Autowired + private CustomerService customerService; + @Autowired + private RedisCache redisCache; + + @GetMapping("/sms/register") + @ApiOperation("用户注册-验证码") + public AjaxResult customerRegister(String phoneNumber) { + String numbers = RandomUtil.randomNumbers(6); + codeService.put(phoneNumber, CodeType.CUSTOMER_REGISTER,numbers); + AjaxResult success = AjaxResult.success(); + success.setData(numbers); + return success; + } + + @PostMapping("/register") + @ApiOperation("用户注册") + public AjaxResult customerRegister(@RequestBody CustomerRegisterReq register) { + if(!codeService.check(register.getPhoneNumber(), CodeType.CUSTOMER_REGISTER,register.getCode())){ + throw new CustomException("验证码错误"); + } + customerService.register(register); + return AjaxResult.success(); + } + + @GetMapping("/sms/forget") + @ApiOperation("忘记密码-验证码") + public AjaxResult customerForgetPassword(String phoneNumber) { + String numbers = RandomUtil.randomNumbers(6); + codeService.put(phoneNumber, CodeType.CUSTOMER_FORGET_PASSWORD,numbers); + AjaxResult success = AjaxResult.success(); + success.setData(numbers); + return success; + } + + @GetMapping("/sms/forget/check") + @ApiOperation("忘记密码-验证码-校验") + public AjaxResult customerForgetPasswordCheck(String phoneNumber,String code) { + if(!codeService.check(phoneNumber, CodeType.CUSTOMER_REGISTER,code)){ + throw new CustomException("验证码错误"); + } + Customer customer = customerService.getCustomerByName(phoneNumber); + String uuid = UUID.randomUUID().toString(); + redisCache.setCacheObject(uuid,customer); + AjaxResult success = AjaxResult.success(); + success.setData(uuid); + return success; + } + + @PostMapping("/updatePwd") + @ApiOperation("修改密码") + public AjaxResult customerForgetPasswordCheck(@RequestBody UpdatePwdOpenReq updatePwdOpenReq) { + Customer customer = redisCache.getCacheObject(updatePwdOpenReq.getCheckCode()); + if(customer == null){ + throw new CustomException("密码修改失败,请重新获取验证码操作"); + } + if(!updatePwdOpenReq.getPassword().equals(updatePwdOpenReq.getConfirmPassword())){ + throw new CustomException("密码不一致"); + } + customerService.updatePwd(customer.getId(),updatePwdOpenReq.getPassword()); + AjaxResult success = AjaxResult.success(); + return success; + } + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/controller/app/AppHomeController.java b/bashi-dk/src/main/java/com/bashi/dk/controller/app/AppHomeController.java new file mode 100644 index 0000000..50f6d49 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/controller/app/AppHomeController.java @@ -0,0 +1,65 @@ + +package com.bashi.dk.controller.app; + +import cn.hutool.core.util.RandomUtil; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.utils.BeanConvertUtil; +import com.bashi.dk.domain.LoansSetting; +import com.bashi.dk.dto.app.req.CalLoanReq; +import com.bashi.dk.dto.app.resp.CalLoanResp; +import com.bashi.dk.dto.app.resp.LoanUser; +import com.bashi.dk.kit.CalLoanManager; +import com.bashi.dk.service.LoansSettingService; +import com.bashi.dk.util.Loan; +import com.bashi.dk.util.PhoneRandomUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.aspectj.weaver.loadtime.Aj; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Random; + +@RestController +@RequestMapping("/app/home/loans") +@Api(value = "首页开放", tags = {"首页开放"}) +public class AppHomeController { + + @Autowired + private CalLoanManager calLoanManager; + @Autowired + private LoansSettingService loansSettingService; + + @PostMapping("/calLoan") + @ApiOperation(value = "计算每月还款") + public AjaxResult calLoan(@RequestBody CalLoanReq calLoanReq) { + if(calLoanReq.getTotalLoanMoney() == null || calLoanReq.getTotalMonth() == null){ + LoansSetting loansSetting = loansSettingService.getLoansSetting(); + if(calLoanReq.getTotalLoanMoney() == null){ + calLoanReq.setTotalLoanMoney(loansSetting.getLoansInitAccount()); + } + if(calLoanReq.getTotalMonth() == null){ + calLoanReq.setTotalMonth(Integer.valueOf(loansSetting.getLoansInitMonth())); + } + } + Loan loan = calLoanManager.calLoan(calLoanReq); + CalLoanResp calLoanResp = BeanConvertUtil.convertTo(loan, CalLoanResp::new); + return AjaxResult.success(calLoanResp); + } + + + @GetMapping("/loansUser") + @ApiOperation(value = "获取贷款用户") + public AjaxResult loansUser() { + Random random = new Random(); + LoanUser loanUser = new LoanUser(); + loanUser.setPhone(PhoneRandomUtil.gen()); + loanUser.setAmount(RandomUtil.randomInt(10,100)+"000"); + loanUser.setTime(LocalDate.now().plusDays(-random.nextInt(7)).format(DateTimeFormatter.ofPattern("yyyy/MM/dd"))); + return AjaxResult.success(loanUser); + } + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/controller/app/AppSettingController.java b/bashi-dk/src/main/java/com/bashi/dk/controller/app/AppSettingController.java new file mode 100644 index 0000000..19f214c --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/controller/app/AppSettingController.java @@ -0,0 +1,74 @@ +package com.bashi.dk.controller.app; + +import cn.hutool.core.util.NumberUtil; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.utils.BeanConvertUtil; +import com.bashi.dk.domain.AgreementSetting; +import com.bashi.dk.domain.HomeSetting; +import com.bashi.dk.domain.LoansSetting; +import com.bashi.dk.dto.app.resp.LoansSettingVO; +import com.bashi.dk.enums.BankTypeEnums; +import com.bashi.dk.service.AgreementSettingService; +import com.bashi.dk.service.HomeSettingService; +import com.bashi.dk.service.LoansSettingService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/app/home/setting") +@Api(value = "设置信息", tags = {"设置信息"}) +public class AppSettingController { + + @Autowired + private AgreementSettingService agreementSettingService; + @Autowired + private HomeSettingService homeSettingService; + @Autowired + private LoansSettingService loansSettingService; + + @GetMapping("/agreement") + @ApiOperation(value = "协议内容") + public AjaxResult agreement() { + AgreementSetting setting = agreementSettingService.getAgreementSetting(); + return AjaxResult.success(setting); + } + + @GetMapping("/home") + @ApiOperation(value = "常规内容") + public AjaxResult home() { + HomeSetting setting = homeSettingService.getHomeSetting(); + return AjaxResult.success(setting); + } + + @GetMapping("/bankType") + @ApiOperation(value = "获取银行列表") + public AjaxResult> bankType(){ + List list = Arrays.stream(BankTypeEnums.values()).map(BankTypeEnums::getName).collect(Collectors.toList()); + return AjaxResult.success(list); + } + + @GetMapping("/loans") + @ApiOperation(value = "贷款信息") + public AjaxResult loans() { + LoansSetting setting = loansSettingService.getLoansSetting(); + LoansSettingVO vo = BeanConvertUtil.convertTo(setting, LoansSettingVO::new); + Double minDayServiceRate = Arrays.stream(setting.getServiceRate().split(",")) + .map(Double::valueOf).min(Double::compareTo).orElse(null); + if(minDayServiceRate != null){ + minDayServiceRate = NumberUtil.div(minDayServiceRate, new Double(30D), 4); + }else{ + minDayServiceRate = 0D; + } + vo.setMinDayServiceRate(minDayServiceRate); + return AjaxResult.success(setting); + } + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/controller/app/LoginV2Controller.java b/bashi-dk/src/main/java/com/bashi/dk/controller/app/LoginV2Controller.java new file mode 100644 index 0000000..4adccfb --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/controller/app/LoginV2Controller.java @@ -0,0 +1,33 @@ +package com.bashi.dk.controller.app; + +import com.bashi.common.constant.Constants; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.domain.model.LoginPhoneBody; +import com.bashi.dk.kit.DkLoginKit; +import com.bashi.framework.security.sms.LoginTypeEnums; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.Map; + +@RestController +public class LoginV2Controller { + @Autowired + private DkLoginKit dkLoginKit; + + @PostMapping("/customer/login") + @ApiOperation("用户登陆") + public AjaxResult loginCustomer(@RequestBody LoginPhoneBody loginBody) { + Map ajax = new HashMap<>(); + loginBody.setLoginRole(LoginTypeEnums.CUSTOMER_PASSWORD.getCode()); + // 生成令牌 + String token = dkLoginKit.login(loginBody); + ajax.put(Constants.TOKEN, token); + return AjaxResult.success(ajax); + } + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/controller/app/V2CommonController.java b/bashi-dk/src/main/java/com/bashi/dk/controller/app/V2CommonController.java new file mode 100644 index 0000000..aaea4bf --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/controller/app/V2CommonController.java @@ -0,0 +1,44 @@ +package com.bashi.dk.controller.app; + +import com.alibaba.fastjson.JSON; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.dk.manager.FileUploadManager; +import com.bashi.dk.manager.FileUploadRes; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.dromara.x.file.storage.core.FileInfo; +import org.dromara.x.file.storage.core.FileStorageService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +/** + *

created on 2021/7/13

+ * + * @author zhangliang + */ +@RestController +@Api(value = "通用接口", tags = {"通用接口"}) +@Slf4j +public class V2CommonController { + @Autowired + private FileStorageService fileStorageService; + + @PostMapping("/v2/common/upload") + @ApiOperation("文件上传") + public AjaxResult uploadFile(MultipartFile file) throws Exception { + try { + FileInfo upload = fileStorageService.of(file).setPath("upload/").upload(); + FileUploadRes fileUploadRes = new FileUploadRes(); + fileUploadRes.setUrl(upload.getUrl()); + fileUploadRes.setFileName(upload.getOriginalFilename()); + log.info("sss={}", JSON.toJSONString(upload)); + return AjaxResult.success(fileUploadRes); + } catch (Exception e) { + return AjaxResult.error(e.getMessage()); + } + } + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/domain/AgreementSetting.java b/bashi-dk/src/main/java/com/bashi/dk/domain/AgreementSetting.java new file mode 100644 index 0000000..a493f0c --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/domain/AgreementSetting.java @@ -0,0 +1,55 @@ +package com.bashi.dk.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; + +/** + * 协议设置 + * @TableName dk_agreement_setting + */ +@TableName(value ="dk_agreement_setting") +@Data +@ApiModel("协议设置对象") +public class AgreementSetting implements Serializable { + /** + * ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 借款协议 + */ + @ApiModelProperty("借款协议") + private String loansAgreement; + + /** + * 服务协议 + */ + @ApiModelProperty("服务协议") + private String serviceAgreement; + + /** + * 授权协议 + */ + @ApiModelProperty("授权协议") + private String authAgreement; + + /** + * 法律责任 + */ + @ApiModelProperty("法律责任") + private String lawAgreement; + + private String contractTemplate; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/domain/Borrow.java b/bashi-dk/src/main/java/com/bashi/dk/domain/Borrow.java new file mode 100644 index 0000000..10c4c12 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/domain/Borrow.java @@ -0,0 +1,210 @@ +package com.bashi.dk.domain; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 借款计划对象 dk_borrow + * + * @author duteliang + * @date 2023-11-29 + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +@TableName("dk_borrow") +@ApiModel("借款计划") +public class Borrow implements Serializable { + + private static final long serialVersionUID=1L; + + + /** ID */ + @TableId(value = "id") + @ApiModelProperty("ID") + private Long id; + + @ApiModelProperty("用途说明") + private String noteRemark; + + + private Boolean auditFlag; + + /** 订单编号 */ + @ApiModelProperty("订单编号") + private String tradeNo; + + /** 总贷款额 */ + @ApiModelProperty("总贷款额") + private BigDecimal totalLoanMoney; + + /** 还款月数 */ + @ApiModelProperty("还款月数") + private Integer totalMonth; + + /** 月利率 */ + @ApiModelProperty("月利率") + private Double loanMonthRate; + + /** 年利率 */ + @ApiModelProperty("年利率") + private Double loanYearRate; + + /** 总利息数 */ + @ApiModelProperty("总利息数") + private BigDecimal totalInterest; + + /** 还款总额 */ + @ApiModelProperty("还款总额") + private BigDecimal totalRepayment; + + /** 首月还款额 */ + @ApiModelProperty("首月还款额") + private BigDecimal firstRepayment; + + /** 每月还款额 */ + @ApiModelProperty("每月还款额") + private BigDecimal avgRepayment; + + /** 每月还款日 */ + @ApiModelProperty("每月还款日") + private Integer dueDate; + + /** 是否打款 */ + @ApiModelProperty("是否打款") + private Integer remitFlag; + + @ApiModelProperty("客户电话") + private String customerPhone; + + /** 借款状态 */ + @ApiModelProperty("借款状态") + private String borrowName; + + @ApiModelProperty("借款状态样式") + private String borrowNameStyle; + + /** 借款说明 */ + @ApiModelProperty("借款说明") + private String borrowRemark; + + /** 还款说明 */ + @ApiModelProperty("还款说明") + private String repayRemark; + + /** 计划 */ + @ApiModelProperty("计划") + private String infoJson; + + /** 客户ID */ + @ApiModelProperty("客户ID") + private Long customerId; + + /** 真实姓名 */ + @ApiModelProperty("真实姓名") + private String realName; + + /** 身份证照片 */ + @ApiModelProperty("身份证照片") + private String cardNum; + + /** 身份证正面 */ + @ApiModelProperty("身份证正面") + private String cardFrontPicture; + + private String handCardPicture; + + /** 身份证背面 */ + @ApiModelProperty("身份证背面") + private String cardBackPicture; + + /** 单位名称 */ + @ApiModelProperty("单位名称") + private String companyName; + + /** 职位 */ + @ApiModelProperty("职位") + private String companyTitle; + + /** 单位电话 */ + @ApiModelProperty("单位电话") + private String companyPhone; + + /** 工作年龄 */ + @ApiModelProperty("工作年龄") + private String companyYear; + + /** 单位地址 */ + @ApiModelProperty("单位地址") + private String companyAddress; + + /** 详细地址 */ + @ApiModelProperty("详细地址") + private String companyAddressInfo; + + /** 现居住地址 */ + @ApiModelProperty("现居住地址") + private String customerAddress; + + /** 详细地址 */ + @ApiModelProperty("详细地址") + private String customerAddressInfo; + + /** 亲属姓名 */ + @ApiModelProperty("亲属姓名") + private String kinsfolkName; + + /** 亲属电话 */ + @ApiModelProperty("亲属电话") + private String kinsfolkPhone; + + /** 亲属关系 1-父母、2-配偶、3-子女,4-祖父母 */ + @ApiModelProperty("亲属关系 1-父母、2-配偶、3-子女,4-祖父母") + private String kinsfolkRef; + + @ApiModelProperty("转账备注") + private String transRemark; + + /** $column.columnComment */ + @ApiModelProperty("$column.columnComment") + private String bankType; + + /** $column.columnComment */ + @ApiModelProperty("$column.columnComment") + private String backCardNum; + + /** $column.columnComment */ + @ApiModelProperty("$column.columnComment") + private String firstBankType; + + /** $column.columnComment */ + @ApiModelProperty("$column.columnComment") + private String firstBackCardNum; + + /** 修改银行卡次数 */ + @ApiModelProperty("修改银行卡次数") + private Integer updateBackNum; + + @ApiModelProperty("收入(万)") + private BigDecimal incomeWan; + + /** 创建时间 */ + @ApiModelProperty("创建时间") + private LocalDateTime createTime; + + /** 修改时间 */ + @ApiModelProperty("修改时间") + private LocalDateTime updateTime; + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/domain/BorrowLog.java b/bashi-dk/src/main/java/com/bashi/dk/domain/BorrowLog.java new file mode 100644 index 0000000..2deb549 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/domain/BorrowLog.java @@ -0,0 +1,21 @@ +package com.bashi.dk.domain; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Data +@NoArgsConstructor +@Accessors(chain = true) +@TableName("dk_borrow_log") +public class BorrowLog { + + /** ID */ + @TableId(value = "id") + private Long id; + private Double withdrawAccount; + private Long customerId; + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/domain/BorrowStatus.java b/bashi-dk/src/main/java/com/bashi/dk/domain/BorrowStatus.java new file mode 100644 index 0000000..d0bbf68 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/domain/BorrowStatus.java @@ -0,0 +1,59 @@ +package com.bashi.dk.domain; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import java.io.Serializable; +import java.util.Date; +import java.math.BigDecimal; + +/** + * 借款状态对象 dk_borrow_status + * + * @author duteliang + * @date 2023-11-29 + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +@TableName("dk_borrow_status") +@ApiModel("借款状态添加对象") +public class BorrowStatus implements Serializable { + + private static final long serialVersionUID=1L; + + + /** ID */ + @TableId(value = "id") + @ApiModelProperty("ID") + private Long id; + + /** 是否可以打款 */ + @ApiModelProperty("是否可以打款") + private Integer usedRemit; + + /** 借款状态 */ + @ApiModelProperty("借款状态") + private String borrowName; + + /** 借款说明 */ + @ApiModelProperty("借款说明") + private String borrowRemark; + + @ApiModelProperty("借款样式") + private String borrowNameStyle; + + /** 创建时间 */ + @TableField(fill = FieldFill.INSERT) + @ApiModelProperty("创建时间") + private Date createTime; + + /** 修改时间 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @ApiModelProperty("修改时间") + private Date updateTime; + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/domain/CustomerInfo.java b/bashi-dk/src/main/java/com/bashi/dk/domain/CustomerInfo.java new file mode 100644 index 0000000..6bd3fa8 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/domain/CustomerInfo.java @@ -0,0 +1,162 @@ +package com.bashi.dk.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.io.Serializable; +import java.math.BigDecimal; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * 客户资料 + * @TableName dk_customer_info + */ +@TableName(value ="dk_customer_info") +@Data +@ApiModel("客户资料") +public class CustomerInfo implements Serializable { + /** + * ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + @ApiModelProperty("身份信息完整度") + private Boolean cardFlag = false; + @ApiModelProperty("资料信息完整度") + private Boolean infoFlag = false; + @ApiModelProperty("银行卡信息完整度") + private Boolean bankFlag = false; + + /** + * 客户ID + */ + @ApiModelProperty("客户ID") + private Long customerId; + + @ApiModelProperty("客户电话") + private String customerPhone; + + /** + * 真实姓名 + */ + @ApiModelProperty("真实姓名") + private String realName; + + /** + * 身份证号码 + */ + @ApiModelProperty("身份证号码") + private String cardNum; + + /** + * 身份证正面 + */ + @ApiModelProperty("身份证正面") + private String cardFrontPicture; + + /** + * 手持身份证照片 + */ + @ApiModelProperty("手持身份证照片") + private String handCardPicture; + + /** + * 身份证背面 + */ + @ApiModelProperty("身份证背面") + private String cardBackPicture; + + /** + * 单位名称 + */ + @ApiModelProperty("单位名称") + private String companyName; + + /** + * 职位 + */ + @ApiModelProperty("职位") + private String companyTitle; + + /** + * 单位电话 + */ + @ApiModelProperty("单位电话") + private String companyPhone; + + /** + * 工作年龄 + */ + @ApiModelProperty("工作年龄") + private String companyYear; + + /** + * 单位地址 + */ + @ApiModelProperty("单位地址") + private String companyAddress; + + /** + * 详细地址 + */ + @ApiModelProperty("详细地址") + private String companyAddressInfo; + + /** + * 现居住地址 + */ + @ApiModelProperty("现居住地址") + private String customerAddress; + + /** + * 详细地址 + */ + @ApiModelProperty("详细地址") + private String customerAddressInfo; + + @ApiModelProperty("收入(万)") + private BigDecimal incomeWan; + + /** + * 亲属姓名 + */ + @ApiModelProperty("亲属姓名") + private String kinsfolkName; + + /** + * 亲属电话 + */ + @ApiModelProperty("亲属电话") + private String kinsfolkPhone; + + /** + * 亲属关系 1-父母、2-配偶、3-子女,4-祖父母 + */ + @ApiModelProperty("亲属关系 1-父母、2-配偶、3-子女,4-祖父母") + private String kinsfolkRef; + + /** + * 开户银行 + */ + @ApiModelProperty("开户银行") + private String bankType; + + /** + * 银行卡号 + */ + @ApiModelProperty("银行卡号") + private String backCardNum; + + @ApiModelProperty("签名") + private String signature; + + private Boolean allowSignature = true; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/domain/HomeSetting.java b/bashi-dk/src/main/java/com/bashi/dk/domain/HomeSetting.java new file mode 100644 index 0000000..8ab015d --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/domain/HomeSetting.java @@ -0,0 +1,42 @@ +package com.bashi.dk.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.io.Serializable; +import lombok.Data; + +/** + * 常规设置 + * @TableName dk_home_setting + */ +@TableName(value ="dk_home_setting") +@Data +public class HomeSetting implements Serializable { + /** + * ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 站点标题 + */ + private String homeTitle; + + /** + * banner图 + */ + private String bannerOne; + + /** + * 公章 + */ + private String commonSeal; + + + private String chatUrl; + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/domain/LoansSetting.java b/bashi-dk/src/main/java/com/bashi/dk/domain/LoansSetting.java new file mode 100644 index 0000000..66e71db --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/domain/LoansSetting.java @@ -0,0 +1,61 @@ +package com.bashi.dk.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.io.Serializable; +import java.math.BigDecimal; +import lombok.Data; + +/** + * 贷款设置 + * @TableName dk_loans_setting + */ +@TableName(value ="dk_loans_setting") +@Data +public class LoansSetting implements Serializable { + /** + * ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 贷款最小金额(元) + */ + private BigDecimal loansMinAccount; + + /** + * 贷款最大金额(元) + */ + private BigDecimal loansMaxAccount; + + /** + * 贷款初始金额(元) + */ + private BigDecimal loansInitAccount; + + /** + * 允许选择月份 + */ + private String loansMonth; + + /** + * 初始选择月份 + */ + private String loansInitMonth; + + /** + * 每月还款日 + */ + private Integer dueDate; + + /** + * 服务费率 + */ + private String serviceRate; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/dto/admin/req/BorrowUpdateStatusReq.java b/bashi-dk/src/main/java/com/bashi/dk/dto/admin/req/BorrowUpdateStatusReq.java new file mode 100644 index 0000000..310efa1 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/dto/admin/req/BorrowUpdateStatusReq.java @@ -0,0 +1,13 @@ +package com.bashi.dk.dto.admin.req; + +import lombok.Data; + +@Data +public class BorrowUpdateStatusReq { + private Long id; + private Integer usedRemit; + private String borrowName; + private String borrowRemark; + private String borrowNameStyle; + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/dto/admin/req/UpdatePwdCustomerReq.java b/bashi-dk/src/main/java/com/bashi/dk/dto/admin/req/UpdatePwdCustomerReq.java new file mode 100644 index 0000000..75e2910 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/dto/admin/req/UpdatePwdCustomerReq.java @@ -0,0 +1,9 @@ +package com.bashi.dk.dto.admin.req; + +import lombok.Data; + +@Data +public class UpdatePwdCustomerReq { + private Long customerId; + private String password; +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/dto/admin/resp/BorrowResp.java b/bashi-dk/src/main/java/com/bashi/dk/dto/admin/resp/BorrowResp.java new file mode 100644 index 0000000..f63a567 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/dto/admin/resp/BorrowResp.java @@ -0,0 +1,10 @@ +package com.bashi.dk.dto.admin.resp; + +import com.bashi.dk.domain.Borrow; +import lombok.Data; + +@Data +public class BorrowResp extends Borrow { + + private String customerLoginName; +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/dto/admin/resp/CustomerAdminResp.java b/bashi-dk/src/main/java/com/bashi/dk/dto/admin/resp/CustomerAdminResp.java new file mode 100644 index 0000000..5b14d7e --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/dto/admin/resp/CustomerAdminResp.java @@ -0,0 +1,11 @@ +package com.bashi.dk.dto.admin.resp; + +import com.bashi.common.core.domain.entity.Customer; +import lombok.Data; + +@Data +public class CustomerAdminResp extends Customer { + private String realName; + + private Boolean allowSignature = true; +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/dto/admin/resp/CustomerExportVo.java b/bashi-dk/src/main/java/com/bashi/dk/dto/admin/resp/CustomerExportVo.java new file mode 100644 index 0000000..38f5a9d --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/dto/admin/resp/CustomerExportVo.java @@ -0,0 +1,116 @@ +package com.bashi.dk.dto.admin.resp; + +import com.bashi.common.annotation.Excel; +import lombok.Data; + +@Data +public class CustomerExportVo { + + @Excel(name = "登陆手机号") + private String phoneNumber; + + /** + * 用户名称 + */ + @Excel(name = "登陆手机号") + private String nickName; + + /** + * 是否实名 + */ + @Excel(name = "是否实名",readConverterExp = "0=否,1=是") + private Integer realNameAuth; + + /** + * 是否贷款 + */ + @Excel(name = "是否贷款",readConverterExp = "0=否,1=是") + private Integer loansFlag; + + /** + * 是否提现 + */ + @Excel(name = "是否提现",readConverterExp = "0=否,1=是") + private Integer withdrawFlag; + + @Excel(name = "真实姓名") + private String realName; + + @Excel(name = "身份证号码") + private String cardNum; + + /** + * 单位名称 + */ + @Excel(name = "单位名称") + private String companyName; + + /** + * 职位 + */ + @Excel(name = "职位") + private String companyTitle; + + /** + * 单位电话 + */ + @Excel(name = "单位电话") + private String companyPhone; + + /** + * 工作年龄 + */ + @Excel(name = "工作年龄") + private String companyYear; + + /** + * 单位地址 + */ + @Excel(name = "单位地址") + private String companyAddress; + + /** + * 详细地址 + */ + @Excel(name = "详细地址") + private String companyAddressInfo; + + /** + * 现居住地址 + */ + @Excel(name = "现居住地址") + private String customerAddress; + + /** + * 详细地址 + */ + @Excel(name = "详细地址") + private String customerAddressInfo; + + @Excel(name = "亲属关系",readConverterExp = "1=父母,2=配偶,3=子女,4=祖父母") + private String kinsfolkRef; + /** + * 亲属姓名 + */ + @Excel(name = "亲属姓名") + private String kinsfolkName; + + /** + * 亲属电话 + */ + @Excel(name = "亲属电话") + private String kinsfolkPhone; + + + /** + * 开户银行 + */ + @Excel(name = "开户银行") + private String bankType; + + /** + * 银行卡号 + */ + @Excel(name = "银行卡号") + private String backCardNum; +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/dto/admin/resp/CustomerInfo.java b/bashi-dk/src/main/java/com/bashi/dk/dto/admin/resp/CustomerInfo.java new file mode 100644 index 0000000..ae570c1 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/dto/admin/resp/CustomerInfo.java @@ -0,0 +1,77 @@ +package com.bashi.dk.dto.admin.resp; + +import com.bashi.common.constant.DateConstant; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class CustomerInfo { + + /** + * ID + */ + private Long id; + + /** + * 手机 + */ + private String phoneNumber; + + /** + * 用户名称 + */ + private String nickName; + + /** + * 用户密码 + */ + private String password; + + /** + * 是否实名 + */ + private Integer realNameAuth; + + /** + * 是否贷款 + */ + private Integer loansFlag; + + /** + * 是否提现 + */ + private Integer withdrawFlag; + + /** + * 余额 + */ + private Long account; + + /** + * 可提现金额 + */ + private Long withdrawAccount; + + /** + * 状态 0-正常 1-封禁 + */ + private Integer status; + + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonFormat(pattern = DateConstant.PATTERN_DATETIME) + private LocalDateTime lastLoginTime; + + private String lastLoginIp; + + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonFormat(pattern = DateConstant.PATTERN_DATETIME) + private LocalDateTime updateTime; +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/dto/app/req/BorrowStartReq.java b/bashi-dk/src/main/java/com/bashi/dk/dto/app/req/BorrowStartReq.java new file mode 100644 index 0000000..9c2ea00 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/dto/app/req/BorrowStartReq.java @@ -0,0 +1,25 @@ +package com.bashi.dk.dto.app.req; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +@ApiModel("发起贷款入参") +public class BorrowStartReq { + + @ApiModelProperty("客户ID") + private Long customerId; + + @ApiModelProperty("用途说明") + private String noteRemark; + + @ApiModelProperty("总贷款额") + private BigDecimal totalLoanMoney; + + @ApiModelProperty("还款月数") + private Integer totalMonth; + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/dto/app/req/CalLoanReq.java b/bashi-dk/src/main/java/com/bashi/dk/dto/app/req/CalLoanReq.java new file mode 100644 index 0000000..43aac31 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/dto/app/req/CalLoanReq.java @@ -0,0 +1,20 @@ +package com.bashi.dk.dto.app.req; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +@ApiModel("贷款计算器") +public class CalLoanReq { + + @ApiModelProperty("总贷款额") + private BigDecimal totalLoanMoney; + + @ApiModelProperty("还款月数") + private Integer totalMonth; + + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/dto/app/req/CustomerRegisterReq.java b/bashi-dk/src/main/java/com/bashi/dk/dto/app/req/CustomerRegisterReq.java new file mode 100644 index 0000000..f160ce1 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/dto/app/req/CustomerRegisterReq.java @@ -0,0 +1,29 @@ +package com.bashi.dk.dto.app.req; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel("用户注册-入参") +public class CustomerRegisterReq { + + /** + * 手机 + */ + @ApiModelProperty("手机号") + private String phoneNumber; + + /** + * 用户密码 + */ + @ApiModelProperty("密码") + private String password; + + /** + * code + */ + @ApiModelProperty("验证码") + private String code; + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/dto/app/req/UpdatePwdOpenReq.java b/bashi-dk/src/main/java/com/bashi/dk/dto/app/req/UpdatePwdOpenReq.java new file mode 100644 index 0000000..6a14bac --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/dto/app/req/UpdatePwdOpenReq.java @@ -0,0 +1,17 @@ +package com.bashi.dk.dto.app.req; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel("用户忘记密码-修改密码入参") +public class UpdatePwdOpenReq { + + @ApiModelProperty("校验码") + private String checkCode; + @ApiModelProperty("密码") + private String password; + @ApiModelProperty("确认密码") + private String confirmPassword; +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/BorrowInfo.java b/bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/BorrowInfo.java new file mode 100644 index 0000000..6f8361e --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/BorrowInfo.java @@ -0,0 +1,14 @@ +package com.bashi.dk.dto.app.resp; + +import com.bashi.dk.domain.Borrow; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel("贷款详情DTO") +public class BorrowInfo extends Borrow { + + @ApiModelProperty("贷款进度条") + private LoanProcessResp loanProcessResp; +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/BorrowStepResp.java b/bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/BorrowStepResp.java new file mode 100644 index 0000000..7c70a79 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/BorrowStepResp.java @@ -0,0 +1,24 @@ +package com.bashi.dk.dto.app.resp; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@ApiModel("贷款进度") +@Data +public class BorrowStepResp { + + @ApiModelProperty("名称") + private String name; + + @ApiModelProperty("是否完成") + private boolean over = false; + + public BorrowStepResp(String name, boolean over) { + this.name = name; + this.over = over; + } + + public BorrowStepResp() { + } +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/CalLoanResp.java b/bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/CalLoanResp.java new file mode 100644 index 0000000..0e0c9bc --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/CalLoanResp.java @@ -0,0 +1,28 @@ +package com.bashi.dk.dto.app.resp; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +@ApiModel("计算利息返回数据") +public class CalLoanResp { + + @ApiModelProperty("贷款总额") + private BigDecimal totalLoanMoney; // + @ApiModelProperty("还款月份") + private int totalMonth; // + @ApiModelProperty("贷款年利率") + private double loanRate; // + + @ApiModelProperty("总利息数") + private BigDecimal totalInterest; // + @ApiModelProperty("还款总额") + private BigDecimal totalRepayment; // + @ApiModelProperty("首月还款额") + private BigDecimal firstRepayment; // + @ApiModelProperty("月均还款额") + private BigDecimal avgRepayment; // +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/LoanProcessResp.java b/bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/LoanProcessResp.java new file mode 100644 index 0000000..555d9c2 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/LoanProcessResp.java @@ -0,0 +1,22 @@ +package com.bashi.dk.dto.app.resp; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.List; + +@Data +@ApiModel("贷款进度说明") +public class LoanProcessResp { + + @ApiModelProperty("贷款进度条") + private List borrowStep; + + @ApiModelProperty("借款状态样式") + private String borrowNameStyle; + + /** 借款说明 */ + @ApiModelProperty("借款说明") + private String borrowRemark; +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/LoanUser.java b/bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/LoanUser.java new file mode 100644 index 0000000..bad405a --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/LoanUser.java @@ -0,0 +1,26 @@ +package com.bashi.dk.dto.app.resp; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel("贷款用户") +public class LoanUser { + /** + * 手机 + */ + @ApiModelProperty("手机号") + private String phone; + /** + * 金额 + */ + @ApiModelProperty("金额") + private String amount; + + /** + * 时间 + */ + @ApiModelProperty("贷款时间") + private String time; +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/LoansSettingVO.java b/bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/LoansSettingVO.java new file mode 100644 index 0000000..7307154 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/dto/app/resp/LoansSettingVO.java @@ -0,0 +1,53 @@ +package com.bashi.dk.dto.app.resp; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +@ApiModel("费率设置-出参") +public class LoansSettingVO { + /** + * 贷款最小金额(元) + */ + @ApiModelProperty("贷款最小金额(元)") + private BigDecimal loansMinAccount; + + /** + * 贷款最大金额(元) + */ + @ApiModelProperty("贷款最大金额(元)") + private BigDecimal loansMaxAccount; + + /** + * 贷款初始金额(元) + */ + @ApiModelProperty("贷款初始金额(元)") + private BigDecimal loansInitAccount; + + /** + * 允许选择月份 + */ + @ApiModelProperty("允许选择月份") + private String loansMonth; + + /** + * 初始选择月份 + */ + @ApiModelProperty("初始选择月份") + private String loansInitMonth; + + /** + * 每月还款日 + */ + @ApiModelProperty("每月还款日") + private Integer dueDate; + + /** + * 最小 日息 + */ + @ApiModelProperty("最小 日息") + private Double minDayServiceRate; +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/enums/BankTypeEnums.java b/bashi-dk/src/main/java/com/bashi/dk/enums/BankTypeEnums.java new file mode 100644 index 0000000..29cf991 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/enums/BankTypeEnums.java @@ -0,0 +1,25 @@ +package com.bashi.dk.enums; + +public enum BankTypeEnums { + PBC("中国人民银行"), + ICBC("中国工商银行"), + CCB("中国建设银行"), + HSBC("汇丰银行"), + BOC("中国银行"), + ABC("中国农业银行"), + BC("交通银行"), + ZS_CMB("招商银行"), + CMB("中国民生银行"), + CITIC("中信银行"), + ; + + private final String name; + + BankTypeEnums(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/kit/CalLoanManager.java b/bashi-dk/src/main/java/com/bashi/dk/kit/CalLoanManager.java new file mode 100644 index 0000000..2916cc6 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/kit/CalLoanManager.java @@ -0,0 +1,42 @@ +package com.bashi.dk.kit; + +import com.bashi.dk.domain.LoansSetting; +import com.bashi.dk.dto.app.req.CalLoanReq; +import com.bashi.dk.service.LoansSettingService; +import com.bashi.dk.util.ACPIMLoanCalculator; +import com.bashi.dk.util.Loan; +import com.bashi.dk.util.LoanUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; + +@Component +public class CalLoanManager { + @Autowired + private LoansSettingService loansSettingService; + + public Loan calLoan(BigDecimal totalLoanMoney,Integer totalMonth, Double loanRate){ + ACPIMLoanCalculator calculator = new ACPIMLoanCalculator(); + Loan loan = calculator.calLoan(totalLoanMoney, totalMonth, loanRate, LoanUtil.RATE_TYPE_MONTH); + loan.setLoanRateMonth(loanRate); + return loan; + } + + public Loan calLoan(CalLoanReq calLoanReq){ + Double loanRate = getLoanRate(calLoanReq.getTotalMonth()); + return this.calLoan(calLoanReq.getTotalLoanMoney(), calLoanReq.getTotalMonth(), loanRate); + } + + public Double getLoanRate(Integer mouth){ + LoansSetting loansSetting = loansSettingService.getLoansSetting(); + String serviceRate = loansSetting.getServiceRate(); + String[] split = serviceRate.split(","); + if(split.length < mouth){ + return Double.valueOf(split[split.length-1]); + }else{ + return Double.valueOf(split[mouth-1]); + } + } + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/kit/DkLoginKit.java b/bashi-dk/src/main/java/com/bashi/dk/kit/DkLoginKit.java new file mode 100644 index 0000000..43b8c27 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/kit/DkLoginKit.java @@ -0,0 +1,80 @@ +package com.bashi.dk.kit; + +import com.bashi.common.constant.Constants; +import com.bashi.common.core.domain.entity.Customer; +import com.bashi.common.core.domain.entity.SysUser; +import com.bashi.common.core.domain.model.LoginPhoneBody; +import com.bashi.common.core.domain.model.LoginUser; +import com.bashi.common.exception.CustomException; +import com.bashi.common.utils.DateUtils; +import com.bashi.common.utils.ServletUtils; +import com.bashi.dk.service.CustomerService; +import com.bashi.framework.security.sms.LoginTypeEnums; +import com.bashi.framework.security.sms.SmsAuthenticationToken; +import com.bashi.framework.web.service.AsyncService; +import com.bashi.framework.web.service.TokenService; +import com.bashi.system.service.ISysUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.time.LocalDateTime; + +@Component +public class DkLoginKit { + + @Resource + private AuthenticationManager authenticationManager; + @Autowired + private AsyncService asyncService; + @Autowired + private TokenService tokenService; + @Autowired + private ISysUserService userService; + @Autowired + private CustomerService customerService; + + public String login(LoginPhoneBody mobile) { + HttpServletRequest request = ServletUtils.getRequest(); + // 用户验证 + Authentication authentication; + try { + authentication = authenticationManager + .authenticate(new SmsAuthenticationToken(mobile)); + } catch (Exception e) { + asyncService.recordLogininfor(mobile.getMobile(), Constants.LOGIN_FAIL, e.getMessage(), request); + throw new CustomException(e.getMessage()); + } + asyncService.recordLogininfor(mobile.getMobile(), Constants.LOGIN_SUCCESS, LoginTypeEnums.getMsgByCode(mobile.getLoginRole()) + "登陆成功", request); + LoginUser loginUser = (LoginUser) authentication.getPrincipal(); + recordLoginInfo(loginUser); + // 生成token + return tokenService.createToken(loginUser); + } + + + /** + * 记录登录信息 + */ + public void recordLoginInfo(LoginUser user) { + if(user.getType() == 1){ + Customer customer = user.getCustomer(); + Customer update = new Customer(); + update.setId(customer.getId()); + update.setLastLoginIp(ServletUtils.getClientIP()); + update.setLastLoginTime(LocalDateTime.now()); + customerService.updateById(update); + }else{ + SysUser sysUser = user.getUser(); + sysUser.setLoginIp(ServletUtils.getClientIP()); + sysUser.setLoginDate(DateUtils.getNowDate()); + sysUser.setUpdateBy(sysUser.getUserName()); + userService.updateUserProfile(sysUser); + } + + } + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/manager/FileUploadManager.java b/bashi-dk/src/main/java/com/bashi/dk/manager/FileUploadManager.java new file mode 100644 index 0000000..d006fa9 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/manager/FileUploadManager.java @@ -0,0 +1,55 @@ +package com.bashi.dk.manager; + +import cn.hutool.core.lang.UUID; +import com.bashi.common.config.BsConfig; +import com.bashi.common.utils.file.FileUploadUtils; +import com.bashi.dk.oss.ali.AliOssKit; +import com.bashi.dk.oss.ali.CosKit; +import com.bashi.framework.config.ServerConfig; +import org.apache.commons.io.FilenameUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +@Component +public class FileUploadManager { + @Autowired + private AliOssKit aliOssKit; + @Autowired + private CosKit cosKit; + @Autowired + private ServerConfig serverConfig; + + + public FileUploadRes uploadFile(MultipartFile file) throws Exception { + if(aliOssKit.getOssClient() != null){ + String filename = file.getOriginalFilename(); + String extension = FilenameUtils.getExtension(filename); + String uuid = UUID.fastUUID().toString(); + String ossUrl = aliOssKit.upload(file.getInputStream(), AliOssKit.OSS_KEY_COMMON + uuid + "." + extension); + FileUploadRes fileUploadRes = new FileUploadRes(); + fileUploadRes.setUrl(ossUrl); + fileUploadRes.setFileName(filename); + return fileUploadRes; + }else if(cosKit.isEnable()){ + String filename = file.getOriginalFilename(); + String extension = FilenameUtils.getExtension(filename); + String uuid = UUID.fastUUID().toString(); + String ossUrl = cosKit.upload(file, AliOssKit.OSS_KEY_COMMON + uuid + "." + extension); + FileUploadRes fileUploadRes = new FileUploadRes(); + fileUploadRes.setUrl(ossUrl); + fileUploadRes.setFileName(filename); + return fileUploadRes; + }else { + // 上传文件路径 + String filePath = BsConfig.getUploadPath(); + // 上传并返回新文件名称 + String fileName = FileUploadUtils.upload(filePath, file); + String url = serverConfig.getUrl() + "/api/" + fileName; + FileUploadRes fileUploadRes = new FileUploadRes(); + fileUploadRes.setUrl(url); + fileUploadRes.setFileName(fileName); + return fileUploadRes; + } + } +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/manager/FileUploadRes.java b/bashi-dk/src/main/java/com/bashi/dk/manager/FileUploadRes.java new file mode 100644 index 0000000..1d39618 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/manager/FileUploadRes.java @@ -0,0 +1,10 @@ +package com.bashi.dk.manager; + +import lombok.Data; + +@Data +public class FileUploadRes { + + private String url; + private String fileName; +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/mapper/AgreementSettingMapper.java b/bashi-dk/src/main/java/com/bashi/dk/mapper/AgreementSettingMapper.java new file mode 100644 index 0000000..ad05c2d --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/mapper/AgreementSettingMapper.java @@ -0,0 +1,7 @@ +package com.bashi.dk.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bashi.dk.domain.AgreementSetting; + +public interface AgreementSettingMapper extends BaseMapper { +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/mapper/BorrowLogMapper.java b/bashi-dk/src/main/java/com/bashi/dk/mapper/BorrowLogMapper.java new file mode 100644 index 0000000..6155099 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/mapper/BorrowLogMapper.java @@ -0,0 +1,7 @@ +package com.bashi.dk.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bashi.dk.domain.BorrowLog; + +public interface BorrowLogMapper extends BaseMapper { +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/mapper/BorrowMapper.java b/bashi-dk/src/main/java/com/bashi/dk/mapper/BorrowMapper.java new file mode 100644 index 0000000..55ffdfb --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/mapper/BorrowMapper.java @@ -0,0 +1,11 @@ +package com.bashi.dk.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.bashi.dk.domain.Borrow; +import com.bashi.dk.dto.admin.resp.BorrowResp; +import org.apache.ibatis.annotations.Param; + +public interface BorrowMapper extends BaseMapper { + IPage pageAdmin(@Param("page") IPage page, @Param("bo") Borrow bo); +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/mapper/BorrowStatusMapper.java b/bashi-dk/src/main/java/com/bashi/dk/mapper/BorrowStatusMapper.java new file mode 100644 index 0000000..f814816 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/mapper/BorrowStatusMapper.java @@ -0,0 +1,7 @@ +package com.bashi.dk.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bashi.dk.domain.BorrowStatus; + +public interface BorrowStatusMapper extends BaseMapper { +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/mapper/CustomerInfoMapper.java b/bashi-dk/src/main/java/com/bashi/dk/mapper/CustomerInfoMapper.java new file mode 100644 index 0000000..269e21b --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/mapper/CustomerInfoMapper.java @@ -0,0 +1,7 @@ +package com.bashi.dk.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bashi.dk.domain.CustomerInfo; + +public interface CustomerInfoMapper extends BaseMapper { +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/mapper/CustomerMapper.java b/bashi-dk/src/main/java/com/bashi/dk/mapper/CustomerMapper.java new file mode 100644 index 0000000..ddc1b60 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/mapper/CustomerMapper.java @@ -0,0 +1,23 @@ +package com.bashi.dk.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.bashi.common.core.domain.entity.Customer; +import com.bashi.dk.dto.admin.resp.CustomerAdminResp; +import com.bashi.dk.dto.admin.resp.CustomerExportVo; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.List; + +public interface CustomerMapper extends BaseMapper { + + void incsAmount(@Param("customerId") Long customerId, @Param("totalLoanMoney") BigDecimal totalLoanMoney, + @Param("totalRepayment") BigDecimal totalRepayment); + + void withdraw(@Param("customerId") Long customerId, @Param("withdrawAmount") Double withdrawAmount); + + IPage pageAdmin(@Param("page") IPage page, @Param("bo") CustomerAdminResp bo); + + List exportAdmin(@Param("bo") CustomerAdminResp bo); +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/mapper/HomeSettingMapper.java b/bashi-dk/src/main/java/com/bashi/dk/mapper/HomeSettingMapper.java new file mode 100644 index 0000000..c77bffd --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/mapper/HomeSettingMapper.java @@ -0,0 +1,7 @@ +package com.bashi.dk.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bashi.dk.domain.HomeSetting; + +public interface HomeSettingMapper extends BaseMapper { +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/mapper/LoansSettingMapper.java b/bashi-dk/src/main/java/com/bashi/dk/mapper/LoansSettingMapper.java new file mode 100644 index 0000000..1dd3dca --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/mapper/LoansSettingMapper.java @@ -0,0 +1,7 @@ +package com.bashi.dk.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bashi.dk.domain.LoansSetting; + +public interface LoansSettingMapper extends BaseMapper { +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/oss/ali/AliOssKit.java b/bashi-dk/src/main/java/com/bashi/dk/oss/ali/AliOssKit.java new file mode 100644 index 0000000..2ca1404 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/oss/ali/AliOssKit.java @@ -0,0 +1,79 @@ +package com.bashi.dk.oss.ali; + +import com.aliyun.oss.ClientConfiguration; +import com.aliyun.oss.OSSClient; +import com.aliyun.oss.common.auth.CredentialsProvider; +import com.aliyun.oss.common.auth.DefaultCredentialProvider; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.io.InputStream; + +/** + *

created on 2021/3/3

+ * + * @author zhangliang + */ +@Component +@Slf4j +public class AliOssKit { + + @Autowired + @Setter + @Getter + private OssConfig ossConfig; + + @Getter + public OSSClient ossClient = null; + + public final static String OSS_KEY_COMMON = "dk/common/"; + + @PostConstruct + public void init(){ + if(!ossConfig.isEnable()){ + log.error("未开启阿里云OSS配置"); + return; + } + // 创建ClientConfiguration。ClientConfiguration是OSSClient的配置类,可配置代理、连接超时、最大连接数等参数。 + ClientConfiguration conf = new ClientConfiguration(); + // 设置OSSClient允许打开的最大HTTP连接数,默认为1024个。 + conf.setMaxConnections(1024); + // 设置Socket层传输数据的超时时间,默认为50000毫秒。 + conf.setSocketTimeout(50000); + // 设置建立连接的超时时间,默认为50000毫秒。 + conf.setConnectionTimeout(50000); + // 设置从连接池中获取连接的超时时间(单位:毫秒),默认不超时。 + conf.setConnectionRequestTimeout(1000); + // 设置连接空闲超时时间。超时则关闭连接,默认为60000毫秒。 + conf.setIdleConnectionTime(60000); + // 设置失败请求重试次数,默认为3次。 + conf.setMaxErrorRetry(5); + CredentialsProvider credentialsProvider = new DefaultCredentialProvider(ossConfig.getAccessKeyId(), ossConfig.getAccessKeySecret()); + // 创建客户端 + ossClient = new OSSClient(ossConfig.getEndpoint(), credentialsProvider, conf); + } + + public String upload(InputStream inputStream,String fileName){ + ossClient.putObject(ossConfig.getBucketName(), fileName, inputStream); + return getUrl(fileName); + } + + /** + * 上传单个文件 公开文件 + * @param inputStream + * @param key + */ + public String uploadByKey(InputStream inputStream,String key){ + ossClient.putObject(ossConfig.getBucketName(), key, inputStream); + return getUrl(key); + } + + public String getUrl(String key){ + return ossConfig.getCdnDomain()+"/"+key; + } + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/oss/ali/CosConfig.java b/bashi-dk/src/main/java/com/bashi/dk/oss/ali/CosConfig.java new file mode 100644 index 0000000..666e4e9 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/oss/ali/CosConfig.java @@ -0,0 +1,23 @@ +package com.bashi.dk.oss.ali; + + +import lombok.Data; +import lombok.ToString; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@ToString(exclude={"secretId","secretKey"}) +@Configuration +@ConfigurationProperties(prefix = "tencent.cos") +public class CosConfig { + + private boolean enable = false; + private String appId; + private String region; // 连接区域地址 + private String cdnDomain; // cdn 域名 + private String secretId; // 连接keyId + private String secretKey; // 连接秘钥 + private String bucketName; // 需要存储的bucketName + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/oss/ali/CosKit.java b/bashi-dk/src/main/java/com/bashi/dk/oss/ali/CosKit.java new file mode 100644 index 0000000..0226225 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/oss/ali/CosKit.java @@ -0,0 +1,105 @@ +package com.bashi.dk.oss.ali; + +import com.qcloud.cos.COSClient; +import com.qcloud.cos.ClientConfig; +import com.qcloud.cos.auth.BasicCOSCredentials; +import com.qcloud.cos.auth.COSCredentials; +import com.qcloud.cos.http.HttpProtocol; +import com.qcloud.cos.model.ObjectMetadata; +import com.qcloud.cos.model.PutObjectRequest; +import com.qcloud.cos.model.UploadResult; +import com.qcloud.cos.region.Region; +import com.qcloud.cos.transfer.TransferManager; +import com.qcloud.cos.transfer.TransferManagerConfiguration; +import com.qcloud.cos.transfer.Upload; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.PostConstruct; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@Component +@Slf4j +public class CosKit { + + @Autowired + private CosConfig cosConfig; + private COSClient cosClient = null; + + private TransferManager transferManager = null; + + + public TransferManager createTransferManager(COSClient cosClient){ + // 自定义线程池大小,建议在客户端与 COS 网络充足(例如使用腾讯云的 CVM,同地域上传 COS)的情况下,设置成16或32即可,可较充分的利用网络资源 + // 对于使用公网传输且网络带宽质量不高的情况,建议减小该值,避免因网速过慢,造成请求超时。 + ExecutorService threadPool = Executors.newFixedThreadPool(4); + // 传入一个 threadpool, 若不传入线程池,默认 TransferManager 中会生成一个单线程的线程池。 + TransferManager transferManager = new TransferManager(cosClient, threadPool); + // 设置高级接口的配置项 + // 分块上传阈值和分块大小分别为 5MB 和 1MB + TransferManagerConfiguration transferManagerConfiguration = new TransferManagerConfiguration(); + transferManagerConfiguration.setMultipartUploadThreshold(5*1024*1024); + transferManagerConfiguration.setMinimumUploadPartSize(1*1024*1024); + transferManager.setConfiguration(transferManagerConfiguration); + return transferManager; + } + + public COSClient createCosClient(){ + // SECRETID 和 SECRETKEY 请登录访问管理控制台 https://console.cloud.tencent.com/cam/capi 进行查看和管理 + String secretId = cosConfig.getSecretId(); + String secretKey = cosConfig.getSecretKey(); + COSCredentials cred = new BasicCOSCredentials(secretId, secretKey); + // ClientConfig 中包含了后续请求 COS 的客户端设置: + ClientConfig clientConfig = new ClientConfig(); + // 设置 bucket 的地域 + // COS_REGION 请参照 https://cloud.tencent.com/document/product/436/6224 + clientConfig.setRegion(new Region(cosConfig.getRegion())); + // 设置请求协议, http 或者 https + // 5.6.53 及更低的版本,建议设置使用 https 协议 + // 5.6.54 及更高版本,默认使用了 https + clientConfig.setHttpProtocol(HttpProtocol.https); + // 以下的设置,是可选的: + // 设置 socket 读取超时,默认 30s + clientConfig.setSocketTimeout(30*1000); + // 设置建立连接超时,默认 30s + clientConfig.setConnectionTimeout(30*1000); + // 如果需要的话,设置 http 代理,ip 以及 port +// clientConfig.setHttpProxyIp("httpProxyIp"); +// clientConfig.setHttpProxyPort(80); + // 生成 cos 客户端。 + return new COSClient(cred, clientConfig); + } + + public boolean isEnable(){ + return cosConfig.isEnable(); + } + + @PostConstruct + public void init(){ + if(!cosConfig.isEnable()){ + log.error("未开启腾讯云COS配置"); + return; + } + cosClient = createCosClient(); + transferManager = createTransferManager(cosClient); + } + + public String upload(MultipartFile file, String fileName) throws Exception { + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(file.getSize()); + PutObjectRequest putObjectRequest = new PutObjectRequest(cosConfig.getBucketName(), fileName, file.getInputStream(),metadata); + Upload upload = transferManager.upload(putObjectRequest); + UploadResult uploadResult = upload.waitForUploadResult(); + return getUrl(uploadResult.getKey()); + } + + + + public String getUrl(String key){ + return cosConfig.getCdnDomain()+"/"+key; + } + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/oss/ali/OssConfig.java b/bashi-dk/src/main/java/com/bashi/dk/oss/ali/OssConfig.java new file mode 100644 index 0000000..89635f2 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/oss/ali/OssConfig.java @@ -0,0 +1,24 @@ +package com.bashi.dk.oss.ali; + +import lombok.Data; +import lombok.ToString; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + *

created on 2021/3/3

+ * + * @author zhangliang + */ +@Data +@ToString(exclude={"accessKeyId","accessKeySecret"}) +@Configuration +@ConfigurationProperties(prefix = "ali.oss") +public class OssConfig { + private boolean enable = false; + private String endpoint; // 连接区域地址 + private String cdnDomain; // cdn 域名 + private String accessKeyId; // 连接keyId + private String accessKeySecret; // 连接秘钥 + private String bucketName; // 需要存储的bucketName +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/service/AgreementSettingService.java b/bashi-dk/src/main/java/com/bashi/dk/service/AgreementSettingService.java new file mode 100644 index 0000000..88b57d8 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/service/AgreementSettingService.java @@ -0,0 +1,8 @@ +package com.bashi.dk.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bashi.dk.domain.AgreementSetting; + +public interface AgreementSettingService extends IService { + AgreementSetting getAgreementSetting(); +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/service/BorrowService.java b/bashi-dk/src/main/java/com/bashi/dk/service/BorrowService.java new file mode 100644 index 0000000..ae0b00f --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/service/BorrowService.java @@ -0,0 +1,32 @@ +package com.bashi.dk.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.service.IService; +import com.bashi.common.com.PageParams; +import com.bashi.dk.domain.Borrow; +import com.bashi.dk.dto.admin.req.BorrowUpdateStatusReq; +import com.bashi.dk.dto.admin.resp.BorrowResp; +import com.bashi.dk.dto.app.req.BorrowStartReq; +import com.bashi.dk.dto.app.resp.LoanProcessResp; + +public interface BorrowService extends IService { + Borrow borrow(BorrowStartReq req); + + Borrow getByTradeNo(String tradeNo); + + boolean updateLoan(Borrow bo); + + boolean updateStatus(BorrowUpdateStatusReq bo); + + boolean updateBank(Borrow bo); + + void withdraw(Double withdrawAmount, Long customerId); + + LoanProcessResp getStepBorrow(Long customerId); + + LoanProcessResp parseStepBorrow(Borrow one); + + IPage pageAdmin(PageParams pageParams, Borrow bo); + + String getContract(String tradeNo); +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/service/BorrowStatusService.java b/bashi-dk/src/main/java/com/bashi/dk/service/BorrowStatusService.java new file mode 100644 index 0000000..c956922 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/service/BorrowStatusService.java @@ -0,0 +1,7 @@ +package com.bashi.dk.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bashi.dk.domain.BorrowStatus; + +public interface BorrowStatusService extends IService { +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/service/CustomerInfoService.java b/bashi-dk/src/main/java/com/bashi/dk/service/CustomerInfoService.java new file mode 100644 index 0000000..a9b77e2 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/service/CustomerInfoService.java @@ -0,0 +1,12 @@ +package com.bashi.dk.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bashi.dk.domain.CustomerInfo; + +public interface CustomerInfoService extends IService { + void updateCustomerInfo(CustomerInfo customerInfo); + + CustomerInfo getByCustomerId(Long customerId); + + boolean updateAllowSignature(CustomerInfo bo); +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/service/CustomerService.java b/bashi-dk/src/main/java/com/bashi/dk/service/CustomerService.java new file mode 100644 index 0000000..912d970 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/service/CustomerService.java @@ -0,0 +1,26 @@ +package com.bashi.dk.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.service.IService; +import com.bashi.common.com.PageParams; +import com.bashi.common.core.domain.entity.Customer; +import com.bashi.dk.dto.admin.resp.CustomerAdminResp; +import com.bashi.dk.dto.app.req.CustomerRegisterReq; + +import java.math.BigDecimal; + +public interface CustomerService extends IService { + Customer getCustomerByName(String mobile); + + void register(CustomerRegisterReq register); + + boolean updatePwd(Long id, String password); + + void borrowAmount(Long customerId, BigDecimal totalLoanMoney,BigDecimal totalRepayment); + + boolean withdraw(Long customerId, Double withdrawAmount); + + void dk(Long customerId); + + IPage pageAdmin(PageParams pageParams, CustomerAdminResp bo); +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/service/HomeSettingService.java b/bashi-dk/src/main/java/com/bashi/dk/service/HomeSettingService.java new file mode 100644 index 0000000..a869d8b --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/service/HomeSettingService.java @@ -0,0 +1,8 @@ +package com.bashi.dk.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bashi.dk.domain.HomeSetting; + +public interface HomeSettingService extends IService { + HomeSetting getHomeSetting(); +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/service/LoansSettingService.java b/bashi-dk/src/main/java/com/bashi/dk/service/LoansSettingService.java new file mode 100644 index 0000000..c2573ca --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/service/LoansSettingService.java @@ -0,0 +1,8 @@ +package com.bashi.dk.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bashi.dk.domain.LoansSetting; + +public interface LoansSettingService extends IService { + LoansSetting getLoansSetting(); +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/service/impl/AgreementSettingServiceImpl.java b/bashi-dk/src/main/java/com/bashi/dk/service/impl/AgreementSettingServiceImpl.java new file mode 100644 index 0000000..c9b38cb --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/service/impl/AgreementSettingServiceImpl.java @@ -0,0 +1,18 @@ +package com.bashi.dk.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bashi.dk.domain.AgreementSetting; +import com.bashi.dk.mapper.AgreementSettingMapper; +import com.bashi.dk.service.AgreementSettingService; +import org.springframework.stereotype.Service; + +@Service +public class AgreementSettingServiceImpl extends ServiceImpl implements AgreementSettingService { + + @Override + public AgreementSetting getAgreementSetting(){ + return this.getOne(Wrappers.lambdaQuery(AgreementSetting.class).last("limit 1")); + } + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/service/impl/BorrowServiceImpl.java b/bashi-dk/src/main/java/com/bashi/dk/service/impl/BorrowServiceImpl.java new file mode 100644 index 0000000..5fec6b0 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/service/impl/BorrowServiceImpl.java @@ -0,0 +1,268 @@ +package com.bashi.dk.service.impl; + +import cn.hutool.core.lang.hash.Hash; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bashi.common.com.Condition; +import com.bashi.common.com.PageParams; +import com.bashi.common.core.domain.entity.Customer; +import com.bashi.common.exception.CustomException; +import com.bashi.common.utils.BeanConvertUtil; +import com.bashi.common.utils.JsonUtils; +import com.bashi.dk.domain.AgreementSetting; +import com.bashi.dk.domain.Borrow; +import com.bashi.dk.domain.CustomerInfo; +import com.bashi.dk.domain.LoansSetting; +import com.bashi.dk.dto.admin.req.BorrowUpdateStatusReq; +import com.bashi.dk.dto.admin.resp.BorrowResp; +import com.bashi.dk.dto.app.req.BorrowStartReq; +import com.bashi.dk.dto.app.req.CalLoanReq; +import com.bashi.dk.dto.app.resp.BorrowStepResp; +import com.bashi.dk.dto.app.resp.LoanProcessResp; +import com.bashi.dk.kit.CalLoanManager; +import com.bashi.dk.mapper.BorrowMapper; +import com.bashi.dk.service.*; +import com.bashi.dk.util.ContentReplaceUtil; +import com.bashi.dk.util.Loan; +import com.bashi.dk.util.MoneyUtil; +import com.bashi.dk.util.OrderTradeNoUtil; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class BorrowServiceImpl extends ServiceImpl implements BorrowService { + + @Autowired + private CustomerService customerService; + @Autowired + private CustomerInfoService customerInfoService; + @Autowired + private CalLoanManager calLoanManager; + @Autowired + private LoansSettingService loansSettingService; + + + + @Override + public Borrow borrow(BorrowStartReq req) { + Customer customer = customerService.getById(req.getCustomerId()); + CustomerInfo customerInfo = customerInfoService.getByCustomerId(req.getCustomerId()); + if(customerInfo == null || customer == null){ + throw new CustomException("用户不存在"); + } + if(customer.getLoansFlag() == 1){ + throw new CustomException("请等待上一次贷款完结后在发起贷款"); + } + if(customer.getRealNameAuth() != 1){ + throw new CustomException("请补全个人资料后在发起贷款"); + } + LoansSetting loansSetting = loansSettingService.getLoansSetting(); + CalLoanReq calLoanReq = new CalLoanReq(); + calLoanReq.setTotalLoanMoney(req.getTotalLoanMoney()); + calLoanReq.setTotalMonth(req.getTotalMonth()); + Loan loan = calLoanManager.calLoan(calLoanReq); + Borrow borrow = BeanConvertUtil.convertTo(customerInfo, Borrow::new); + borrow.setId(null); + borrow.setTradeNo(OrderTradeNoUtil.createOrder(OrderTradeNoUtil.BORROW)); + borrow.setTotalLoanMoney(loan.getTotalLoanMoney()); + borrow.setTotalMonth(loan.getTotalMonth()); + borrow.setLoanMonthRate(loan.getLoanRateMonth()); + borrow.setLoanYearRate(loan.getLoanRate()); + borrow.setTotalInterest(loan.getTotalInterest()); + borrow.setTotalRepayment(loan.getTotalRepayment()); + borrow.setFirstRepayment(loan.getFirstRepayment()); + borrow.setAvgRepayment(loan.getAvgRepayment()); + borrow.setDueDate(loansSetting.getDueDate()); + borrow.setNoteRemark(req.getNoteRemark()); + borrow.setBorrowName("审核中"); + borrow.setBorrowNameStyle("black"); + borrow.setBorrowRemark("审核中..."); + borrow.setInfoJson(JsonUtils.toJsonString(loan)); + borrow.setFirstBankType(borrow.getBankType()); + borrow.setFirstBackCardNum(borrow.getBackCardNum()); + this.save(borrow); + customerService.dk(customer.getId()); + return borrow; + } + + @Override + public Borrow getByTradeNo(String tradeNo) { + return this.getOne(Wrappers.lambdaQuery(Borrow.class) + .eq(Borrow::getTradeNo,tradeNo).last("limit 1")); + } + + @Override + public boolean updateLoan(Borrow bo) { + Loan loan = calLoanManager.calLoan(bo.getTotalLoanMoney(),bo.getTotalMonth(),bo.getLoanMonthRate()); + Borrow update = new Borrow(); + update.setId(bo.getId()); + update.setTotalLoanMoney(loan.getTotalLoanMoney()); + update.setTotalMonth(loan.getTotalMonth()); + update.setLoanMonthRate(loan.getLoanRateMonth()); + update.setLoanYearRate(loan.getLoanRate()); + update.setTotalInterest(loan.getTotalInterest()); + update.setTotalRepayment(loan.getTotalRepayment()); + update.setFirstRepayment(loan.getFirstRepayment()); + update.setAvgRepayment(loan.getAvgRepayment()); + update.setInfoJson(JsonUtils.toJsonString(loan)); + return this.updateById(update); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateStatus(BorrowUpdateStatusReq bo) { + Borrow borrow = this.getById(bo.getId()); + boolean remit = false; + if(borrow.getRemitFlag() == 0 && bo.getUsedRemit() != null && bo.getUsedRemit() == 1){ + remit = true; + } + Borrow update = new Borrow(); + update.setId(bo.getId()); + update.setBorrowName(bo.getBorrowName()); + update.setBorrowRemark(bo.getBorrowRemark()); + update.setBorrowNameStyle(bo.getBorrowNameStyle()); + update.setAuditFlag(true); + if(remit){ + update.setRemitFlag(1); + } + boolean bool = this.updateById(update); + if(bool && remit){ + customerService.borrowAmount(borrow.getCustomerId(), borrow.getTotalLoanMoney(),borrow.getTotalRepayment()); + return true; + } + return bool; + } + + @Override + public boolean updateBank(Borrow bo) { + this.updateById(bo); + Borrow borrow = this.getById(bo.getId()); + LambdaUpdateWrapper update = Wrappers.lambdaUpdate(CustomerInfo.class).eq(CustomerInfo::getCustomerId, borrow.getCustomerId()); + boolean updateFlag = false; + if(StringUtils.isNotEmpty(bo.getBankType())){ + update.set(CustomerInfo::getBankType,bo.getBankType()); + updateFlag = true; + } + if(StringUtils.isNotEmpty(bo.getBackCardNum())){ + update.set(CustomerInfo::getBackCardNum,bo.getBackCardNum()); + updateFlag = true; + } + if(updateFlag){ + customerInfoService.update(update); + } + return true; + } + + @Override + public void withdraw(Double withdrawAmount, Long customerId) { + Customer customer = customerService.getById(customerId); + if(BooleanUtils.isNotTrue(customer.getAllowWithdrawFlag())){ + throw new CustomException("提现失败,账号异常"); + } + if(customer.getAccount().doubleValue() < withdrawAmount){ + throw new CustomException("余额不足"); + } + Borrow one = this.getOne(Wrappers.lambdaQuery(Borrow.class).eq(Borrow::getCustomerId,customerId)); + if(one == null){ + throw new CustomException("提现失败"); + } + if(!"审核通过".equals(one.getBorrowName())){ + throw new CustomException(one.getBorrowName()); + } + boolean result = customerService.withdraw(customerId,withdrawAmount); + Borrow update = new Borrow(); + update.setId(one.getId()); + update.setBorrowName("提现中"); + update.setBorrowRemark("提现中..."); + update.setBorrowNameStyle("red"); + this.updateById(update); + } + + @Override + public LoanProcessResp getStepBorrow(Long customerId){ + Borrow one = this.getOne(Wrappers.lambdaQuery(Borrow.class).eq(Borrow::getCustomerId,customerId)); + return parseStepBorrow(one); + } + + @Override + public LoanProcessResp parseStepBorrow(Borrow one){ + LoanProcessResp resp = new LoanProcessResp(); + List borrowStep = new ArrayList<>(); + if(one == null){ + borrowStep.add(new BorrowStepResp("提交成功",false)); + borrowStep.add(new BorrowStepResp("正在审核",false)); + borrowStep.add(new BorrowStepResp("到账成功",false)); + resp.setBorrowStep(borrowStep); + resp.setBorrowNameStyle("red"); + resp.setBorrowRemark("您的个人信用贷款正在审核,请留意您的审核状态!如有疑问,请联系业务员咨询…"); + return resp; + } + borrowStep.add(new BorrowStepResp("提交成功",true)); + if("审核通过".equals(one.getBorrowName())){ + borrowStep.add(new BorrowStepResp(one.getBorrowName(),true)); + borrowStep.add(new BorrowStepResp("到账成功",true)); + }else{ + borrowStep.add(new BorrowStepResp(one.getBorrowName(),true)); + borrowStep.add(new BorrowStepResp("到账成功",false)); + } + resp.setBorrowStep(borrowStep); + resp.setBorrowNameStyle(one.getBorrowNameStyle()); + resp.setBorrowRemark(one.getBorrowRemark()); + return resp; + } + + @Override + public IPage pageAdmin(PageParams pageParams, Borrow bo) { + return baseMapper.pageAdmin(Condition.getPage(pageParams),bo); + } + + @Autowired + private AgreementSettingService agreementSettingService; + + @Override + public String getContract(String tradeNo) { + Borrow borrow = this.getByTradeNo(tradeNo); + if(borrow == null){ + throw new CustomException("借款不存在"); + } + CustomerInfo customerInfo = customerInfoService.getByCustomerId(borrow.getCustomerId()); + Customer customer = customerService.getById(borrow.getCustomerId()); + LocalDate startDate = borrow.getCreateTime().toLocalDate(); + Map map = new HashMap<>(); + map.put("合同编号",borrow.getTradeNo()); + map.put("签订日期",startDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); + map.put("借款人用户名",StringUtils.isEmpty(customer.getPhoneNumber())?"":customer.getPhoneNumber()); + map.put("借款人身份证号",StringUtils.isEmpty(borrow.getCardNum())?"":borrow.getCardNum()); + map.put("借款人手机号",StringUtils.isEmpty(customer.getPhoneNumber())?"":customer.getPhoneNumber()); + String signatureImage = "
签名:
"; + if(StringUtils.isNotEmpty(customerInfo.getSignature())){ +// signatureImage = "\"无效\""; + signatureImage = "
签名:\"无效\"
"; + } + map.put("借款人签名",signatureImage); + map.put("借款金额大写", MoneyUtil.toChinese(borrow.getTotalLoanMoney().toString())); + map.put("借款金额小写",borrow.getTotalLoanMoney().toString()); + map.put("借款期限",borrow.getTotalMonth()); + map.put("借款开始日",startDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); + map.put("借款结束日",startDate.plusMonths(borrow.getTotalMonth()).format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); + map.put("借款人名称",StringUtils.isEmpty(borrow.getRealName())?"":borrow.getRealName()); + AgreementSetting agreementSetting = agreementSettingService.getAgreementSetting(); + String contractTemplate = agreementSetting.getContractTemplate(); + return ContentReplaceUtil.replaceWord(contractTemplate, map); + } + + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/service/impl/BorrowStatusServiceImpl.java b/bashi-dk/src/main/java/com/bashi/dk/service/impl/BorrowStatusServiceImpl.java new file mode 100644 index 0000000..c8a1bf9 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/service/impl/BorrowStatusServiceImpl.java @@ -0,0 +1,11 @@ +package com.bashi.dk.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bashi.dk.domain.BorrowStatus; +import com.bashi.dk.mapper.BorrowStatusMapper; +import com.bashi.dk.service.BorrowStatusService; +import org.springframework.stereotype.Service; + +@Service +public class BorrowStatusServiceImpl extends ServiceImpl implements BorrowStatusService { +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/service/impl/CustomerInfoServiceImpl.java b/bashi-dk/src/main/java/com/bashi/dk/service/impl/CustomerInfoServiceImpl.java new file mode 100644 index 0000000..337c0ea --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/service/impl/CustomerInfoServiceImpl.java @@ -0,0 +1,86 @@ +package com.bashi.dk.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bashi.common.core.domain.entity.Customer; +import com.bashi.dk.domain.CustomerInfo; +import com.bashi.dk.mapper.CustomerInfoMapper; +import com.bashi.dk.service.CustomerInfoService; +import com.bashi.dk.service.CustomerService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class CustomerInfoServiceImpl extends ServiceImpl implements CustomerInfoService { + @Autowired + private CustomerService customerService; + @Override + public void updateCustomerInfo(CustomerInfo customerInfo) { + Long customerId = customerInfo.getCustomerId(); + this.update(customerInfo,Wrappers.lambdaQuery(CustomerInfo.class) + .eq(CustomerInfo::getCustomerId, customerId)); + CustomerInfo one = this.getOne(Wrappers.lambdaQuery(CustomerInfo.class) + .eq(CustomerInfo::getCustomerId, customerId)); + checkCustomerInfoFlag(one); + } + + private void checkCustomerInfoFlag(CustomerInfo one){ + Long customerId = one.getCustomerId(); + int realNameAuth = 0; + boolean infoFlag = false; + boolean cardFlag = false; + boolean bankFlag = false; + boolean signFlag = false; + // 身份信息 + if(StringUtils.isNotEmpty(one.getRealName()) && StringUtils.isNotEmpty(one.getCardNum()) && + StringUtils.isNotEmpty(one.getCardFrontPicture()) && StringUtils.isNotEmpty(one.getCardBackPicture()) && + StringUtils.isNotEmpty(one.getHandCardPicture())){ + cardFlag = true; + } + // 资料信息 + if(StringUtils.isNotEmpty(one.getCompanyName()) && StringUtils.isNotEmpty(one.getCompanyTitle()) && + StringUtils.isNotEmpty(one.getCompanyPhone()) && StringUtils.isNotEmpty(one.getCompanyYear()) && + StringUtils.isNotEmpty(one.getCompanyAddress()) && StringUtils.isNotEmpty(one.getCompanyAddressInfo()) && + StringUtils.isNotEmpty(one.getKinsfolkName()) && StringUtils.isNotEmpty(one.getKinsfolkRef()) && + StringUtils.isNotEmpty(one.getKinsfolkPhone()) && one.getIncomeWan() != null){ + infoFlag = true; + } + // 收款银行卡 + if(StringUtils.isNotEmpty(one.getBankType()) && StringUtils.isNotEmpty(one.getBackCardNum())){ + bankFlag = true; + } + if(!one.getAllowSignature()){ + signFlag = true; + } else { + signFlag = StringUtils.isNotEmpty(one.getSignature()); + } + if(infoFlag && cardFlag && bankFlag && signFlag){ + realNameAuth = 1; + } + this.update(Wrappers.lambdaUpdate(CustomerInfo.class) + .eq(CustomerInfo::getId, customerId) + .set(CustomerInfo::getInfoFlag,infoFlag) + .set(CustomerInfo::getCardFlag,cardFlag) + .set(CustomerInfo::getBankFlag,bankFlag)); + customerService.update(Wrappers.lambdaUpdate(Customer.class) + .eq(Customer::getId, customerId) + .set(Customer::getRealNameAuth,realNameAuth)); + } + + @Override + public CustomerInfo getByCustomerId(Long customerId) { + return this.getOne(Wrappers.lambdaQuery(CustomerInfo.class) + .eq(CustomerInfo::getCustomerId,customerId)); + } + + @Override + public boolean updateAllowSignature(CustomerInfo bo) { + this.update(Wrappers.lambdaUpdate(CustomerInfo.class) + .eq(CustomerInfo::getCustomerId,bo.getCustomerId()) + .set(CustomerInfo::getAllowSignature,bo.getAllowSignature())); + CustomerInfo customerInfo = this.getByCustomerId(bo.getCustomerId()); + checkCustomerInfoFlag(customerInfo); + return false; + } +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/service/impl/CustomerServiceImpl.java b/bashi-dk/src/main/java/com/bashi/dk/service/impl/CustomerServiceImpl.java new file mode 100644 index 0000000..5d57c63 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/service/impl/CustomerServiceImpl.java @@ -0,0 +1,93 @@ +package com.bashi.dk.service.impl; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bashi.common.com.Condition; +import com.bashi.common.com.PageParams; +import com.bashi.common.core.domain.entity.Customer; +import com.bashi.common.exception.CustomException; +import com.bashi.dk.domain.BorrowLog; +import com.bashi.dk.domain.CustomerInfo; +import com.bashi.dk.dto.admin.resp.CustomerAdminResp; +import com.bashi.dk.dto.app.req.CustomerRegisterReq; +import com.bashi.dk.mapper.BorrowLogMapper; +import com.bashi.dk.mapper.CustomerMapper; +import com.bashi.dk.service.CustomerInfoService; +import com.bashi.dk.service.CustomerService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.math.BigDecimal; + +@Service +public class CustomerServiceImpl extends ServiceImpl implements CustomerService { + @Autowired + private BCryptPasswordEncoder passwordEncoder; + @Autowired + private CustomerInfoService customerInfoService; + @Resource + private BorrowLogMapper borrowLogMapper; + @Override + public Customer getCustomerByName(String mobile) { + return this.getOne(Wrappers.lambdaQuery(Customer.class) + .eq(Customer::getPhoneNumber,mobile) + .last("limit 1")); + } + + @Override + public void register(CustomerRegisterReq register) { + String phoneNumber = register.getPhoneNumber(); + Customer customer = this.getCustomerByName(phoneNumber); + if(customer != null){ + throw new CustomException("用户已存在"); + } + customer = new Customer(); + customer.setPhoneNumber(phoneNumber); + customer.setNickName("VIP用户"+phoneNumber.substring(phoneNumber.length() - 4)); + customer.setPassword(passwordEncoder.encode(register.getPassword())); + this.save(customer); + CustomerInfo customerInfo = new CustomerInfo(); + customerInfo.setCustomerId(customer.getId()); + customerInfoService.save(customerInfo); + } + + @Override + public boolean updatePwd(Long id, String password) { + return this.update(Wrappers.lambdaUpdate(Customer.class) + .eq(Customer::getId,id) + .set(Customer::getPassword,passwordEncoder.encode(password))); + } + + @Override + public void borrowAmount(Long customerId, BigDecimal totalLoanMoney,BigDecimal totalRepayment) { + baseMapper.incsAmount(customerId,totalLoanMoney,totalRepayment); + } + + @Override + public boolean withdraw(Long customerId, Double withdrawAmount) { + baseMapper.withdraw(customerId,withdrawAmount); + BorrowLog borrowLog = new BorrowLog(); + borrowLog.setWithdrawAccount(withdrawAmount); + borrowLog.setCustomerId(customerId); + borrowLogMapper.insert(borrowLog); + this.update(Wrappers.lambdaUpdate(Customer.class) + .eq(Customer::getId,customerId) + .set(Customer::getWithdrawFlag,1)); + return true; + } + + @Override + public void dk(Long customerId){ + this.update(Wrappers.lambdaUpdate(Customer.class) + .eq(Customer::getId,customerId) + .set(Customer::getLoansFlag,1)); + } + + @Override + public IPage pageAdmin(PageParams pageParams, CustomerAdminResp bo) { + return baseMapper.pageAdmin(Condition.getPage(pageParams),bo); + } +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/service/impl/HomeSettingServiceImpl.java b/bashi-dk/src/main/java/com/bashi/dk/service/impl/HomeSettingServiceImpl.java new file mode 100644 index 0000000..55b30b1 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/service/impl/HomeSettingServiceImpl.java @@ -0,0 +1,16 @@ +package com.bashi.dk.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bashi.dk.domain.HomeSetting; +import com.bashi.dk.mapper.HomeSettingMapper; +import com.bashi.dk.service.HomeSettingService; +import org.springframework.stereotype.Service; + +@Service +public class HomeSettingServiceImpl extends ServiceImpl implements HomeSettingService { + @Override + public HomeSetting getHomeSetting(){ + return this.getOne(Wrappers.lambdaQuery(HomeSetting.class).last("limit 1")); + } +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/service/impl/LoansSettingServiceImpl.java b/bashi-dk/src/main/java/com/bashi/dk/service/impl/LoansSettingServiceImpl.java new file mode 100644 index 0000000..dba4be3 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/service/impl/LoansSettingServiceImpl.java @@ -0,0 +1,16 @@ +package com.bashi.dk.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bashi.dk.domain.LoansSetting; +import com.bashi.dk.mapper.LoansSettingMapper; +import com.bashi.dk.service.LoansSettingService; +import org.springframework.stereotype.Service; + +@Service +public class LoansSettingServiceImpl extends ServiceImpl implements LoansSettingService { + @Override + public LoansSetting getLoansSetting(){ + return this.getOne(Wrappers.lambdaQuery(LoansSetting.class).last("limit 1")); + } +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/util/ACMLoanCalculator.java b/bashi-dk/src/main/java/com/bashi/dk/util/ACMLoanCalculator.java new file mode 100644 index 0000000..cb4c834 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/util/ACMLoanCalculator.java @@ -0,0 +1,62 @@ +package com.bashi.dk.util; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +/** + * 等额本金还款法 + * + * Created by WangGenshen on 1/23/16. + */ +public class ACMLoanCalculator extends LoanCalculatorAdapter { + + @Override + public Loan calLoan(BigDecimal totalLoanMoney, int totalMonth, double loanRate, int rateType) { + Loan loan = new Loan(); + BigDecimal loanRateMonth = rateType == LoanUtil.RATE_TYPE_YEAR ? new BigDecimal(loanRate / 100 / 12) : new BigDecimal(loanRate / 100); + loan.setTotalMonth(totalMonth); + loan.setTotalLoanMoney(totalLoanMoney); + BigDecimal payPrincipal = totalLoanMoney.divide(new BigDecimal(totalMonth), 2, BigDecimal.ROUND_HALF_UP); + + BigDecimal totalPayedPrincipal = new BigDecimal(0);//累积所还本金 + BigDecimal totalInterest = new BigDecimal(0); //总利息 + BigDecimal totalRepayment = new BigDecimal(0); // 已还款总数 + List loanByMonthList = new ArrayList<>(); + int year = 0; + int monthInYear = 0; + for (int i = 0; i < totalMonth; i++) { + LoanByMonth loanByMonth = new LoanByMonth(); + loanByMonth.setMonth(i + 1); + loanByMonth.setYear(year + 1); + loanByMonth.setMonthInYear(++monthInYear); + if ((i + 1) % 12 == 0) { + year++; + monthInYear = 0; + } + totalPayedPrincipal = totalPayedPrincipal.add(payPrincipal); + loanByMonth.setPayPrincipal(payPrincipal); + BigDecimal interest = totalLoanMoney.subtract(totalPayedPrincipal).multiply(loanRateMonth).setScale(2, BigDecimal.ROUND_HALF_UP); + loanByMonth.setInterest(interest); + totalInterest = totalInterest.add(interest); + loanByMonth.setRepayment(payPrincipal.add(interest)); + if (i == 0) { + loan.setFirstRepayment(loanByMonth.getRepayment()); + } + totalRepayment = totalRepayment.add(loanByMonth.getRepayment()); + loanByMonth.setRemainPrincipal(totalLoanMoney.subtract(totalPayedPrincipal)); + loanByMonthList.add(loanByMonth); + } + loan.setTotalRepayment(totalRepayment); + loan.setAvgRepayment(totalRepayment.divide(new BigDecimal(totalMonth), 2, BigDecimal.ROUND_HALF_UP)); + loan.setTotalInterest(totalInterest); + BigDecimal totalPayedRepayment = new BigDecimal(0); + for (LoanByMonth loanByMonth : loanByMonthList) { + totalPayedRepayment = totalPayedRepayment.add(loanByMonth.getRepayment()); + loanByMonth.setRemainTotal(totalRepayment.subtract(totalPayedRepayment)); + } + loan.setAllLoans(loanByMonthList); + return loan; + } + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/util/ACPIMLoanCalculator.java b/bashi-dk/src/main/java/com/bashi/dk/util/ACPIMLoanCalculator.java new file mode 100644 index 0000000..bf6bdb1 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/util/ACPIMLoanCalculator.java @@ -0,0 +1,59 @@ +package com.bashi.dk.util; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +/** + * 等额本息还款法 + * Created by WangGenshen on 1/23/16. + */ +public class ACPIMLoanCalculator extends LoanCalculatorAdapter { + + @Override + public Loan calLoan(BigDecimal totalLoanMoney, int totalMonth, double loanRate, int rateType) { + Loan loan = new Loan(); + BigDecimal loanRateMonth = rateType == LoanUtil.RATE_TYPE_YEAR ? new BigDecimal(loanRate / 100 / 12) : new BigDecimal(loanRate / 100); + BigDecimal factor = new BigDecimal(Math.pow(1 + loanRateMonth.doubleValue(), totalMonth)); + BigDecimal avgRepayment = totalLoanMoney.multiply(loanRateMonth).multiply(factor).divide(factor.subtract(new BigDecimal(1)), 2, BigDecimal.ROUND_HALF_UP); + loan.setLoanRate(loanRate); + loan.setTotalLoanMoney(totalLoanMoney); + loan.setTotalMonth(totalMonth); + loan.setAvgRepayment(avgRepayment); + loan.setTotalRepayment(avgRepayment.multiply(new BigDecimal(totalMonth))); + loan.setFirstRepayment(avgRepayment); + + BigDecimal totalPayedPrincipal = new BigDecimal(0);//累积所还本金 + BigDecimal totalInterest = new BigDecimal(0); //总利息 + BigDecimal totalRepayment = new BigDecimal(0); // 已还款总数 + List loanByMonthList = new ArrayList<>(); + int year = 0; + int monthInYear = 0; + for (int i = 0; i < totalMonth; i++) { + LoanByMonth loanByMonth = new LoanByMonth(); + BigDecimal remainPrincipal = totalLoanMoney.subtract(totalPayedPrincipal); + BigDecimal interest = remainPrincipal.multiply(loanRateMonth).setScale(2, BigDecimal.ROUND_HALF_UP); + totalInterest = totalInterest.add(interest); + BigDecimal principal = loan.getAvgRepayment().subtract(interest); + totalPayedPrincipal = totalPayedPrincipal.add(principal); + loanByMonth.setMonth(i + 1); + loanByMonth.setYear(year + 1); + loanByMonth.setMonthInYear(++monthInYear); + if ((i + 1) % 12 == 0) { + year++; + monthInYear = 0; + } + loanByMonth.setInterest(interest); + loanByMonth.setPayPrincipal(principal); + loanByMonth.setRepayment(loan.getAvgRepayment()); + totalRepayment = totalRepayment.add(loanByMonth.getRepayment()); + loanByMonth.setRemainPrincipal(remainPrincipal); + loanByMonth.setRemainTotal(loan.getTotalRepayment().subtract(totalRepayment)); + loanByMonthList.add(loanByMonth); + } + loan.setTotalInterest(totalInterest); + loan.setAllLoans(loanByMonthList); + return loan; + } + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/util/ContentReplaceUtil.java b/bashi-dk/src/main/java/com/bashi/dk/util/ContentReplaceUtil.java new file mode 100644 index 0000000..a383088 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/util/ContentReplaceUtil.java @@ -0,0 +1,46 @@ +package com.bashi.dk.util; + +import cn.hutool.core.util.StrUtil; +import freemarker.cache.StringTemplateLoader; +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import lombok.SneakyThrows; +import org.springframework.ui.freemarker.FreeMarkerTemplateUtils; + +import java.io.IOException; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * 使用说明 ${name!12312312} 代表默认值 + * 支持自定义日期格式newDate为约定 ${newDate?string("yyyy-MM-dd HH:mm:ss") + * 普通字符直接替换${code} + * @author zlf + * @date 2023/6/19 6:56 PM + * @desc + */ +public class ContentReplaceUtil { + + public static String NOW_DATE = "newDate"; + + + @SneakyThrows + public static String replaceWord(String content, Map params) { + if (StrUtil.isEmpty(content)){ + return null; + } + if (content.contains(NOW_DATE)){ + params.put("newDate",new Date()); + } + StringTemplateLoader stringTemplateLoader = new StringTemplateLoader(); + stringTemplateLoader.putTemplate("test.ftl", content); + Configuration cfg = new Configuration(Configuration.VERSION_2_3_22); + cfg.setTemplateLoader(stringTemplateLoader); + Template template = cfg.getTemplate("test.ftl"); + String finalContent = FreeMarkerTemplateUtils.processTemplateIntoString(template, params); + return finalContent; + } + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/util/ILoanCalculator.java b/bashi-dk/src/main/java/com/bashi/dk/util/ILoanCalculator.java new file mode 100644 index 0000000..dbe076b --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/util/ILoanCalculator.java @@ -0,0 +1,21 @@ +package com.bashi.dk.util; + +import java.math.BigDecimal; + +/** + * Created by WangGenshen on 1/14/16. + */ +public interface ILoanCalculator { + + /** + * 贷款计算 + * + * @param totalLoanMoney 总贷款额 + * @param totalMonth 还款月数 + * @param loanRate 贷款利率 + * @param rateType 可选择年利率或月利率 + * @return + */ + public Loan calLoan(BigDecimal totalLoanMoney, int totalMonth, double loanRate, int rateType); + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/util/ImageUtil.java b/bashi-dk/src/main/java/com/bashi/dk/util/ImageUtil.java new file mode 100644 index 0000000..7d2a7de --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/util/ImageUtil.java @@ -0,0 +1,154 @@ +/* +package com.bashi.dk.util; + +import com.sun.image.codec.jpeg.JPEGCodec; +import com.sun.image.codec.jpeg.JPEGEncodeParam; +import com.sun.image.codec.jpeg.JPEGImageEncoder; +import lombok.extern.slf4j.Slf4j; +import sun.font.FontDesignMetrics; + +import javax.imageio.ImageIO; +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +@Slf4j +public class ImageUtil { + + public static void main(String[] args) { + String filePath = "d:\\Users\\004796\\桌面\\123\\hd_bg.png"; + Map content = new LinkedHashMap<>(); + content.put(" 转账批次号","542838541"); + content.put("  转出单位","招商银行股份有限公司"); + content.put("  转出账户","2361293892173872198"); + content.put("转出账号地区","深圳市福田区深南大道708"); + content.put(" 收款人姓名","马中华"); + content.put("  收款账户","1273891273897123718"); + content.put("    币种","人民币元"); + content.put("  转出金额","100000.00"); + content.put("  转出时间","2023-11-27 09:30:12"); + content.put("  转账类型","签约金融企业--网贷放款预约到账"); + content.put("  执行方式","点击选择执行方式"); + content.put("    状态","点击选择状态"); + content.put("  银行备注","点击选择银行备注"); + content.put("  处理结果","点击选择处理结果"); + content.put("  用户备注","点击选择用户备注"); + String putPath = "d:\\Users\\004796\\桌面\\123\\1233.jpg"; + createStringMark(filePath,content,putPath); + } + + + //给jpg添加文字 + public static boolean createStringMark(String filePath, Map content, String outPath) { + ImageIcon imgIcon = new ImageIcon(filePath); + Image theImg = imgIcon.getImage(); + int width = theImg.getWidth(null) == -1 ? 200 : theImg.getWidth(null); + int height = theImg.getHeight(null) == -1 ? 200 : theImg.getHeight(null); + int fontSize = 16; + BufferedImage bimage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics2D g = bimage.createGraphics(); + Color mycolor = Color.black; + g.setColor(mycolor); + g.setBackground(Color.black); + g.drawImage(theImg, 0, 0, null); + g.setFont(new Font("宋体", Font.PLAIN, fontSize)); //字体、字型、字号 + RenderingHints hints = new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + hints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g.setRenderingHints(hints); + Graphics2D g1 = bimage.createGraphics(); + g1.setColor(mycolor); + g1.setBackground(Color.black); + g1.drawImage(theImg, 0, 0, null); + g1.setFont(new Font("黑体", Font.PLAIN, fontSize)); //字体、字型、字号 + g1.setRenderingHints(hints); + int widthFlag = 500; + int heightFlag = 335; + for (Map.Entry entry : content.entrySet()) { + g1.drawString(entry.getKey()+": "+entry.getValue(), widthFlag, heightFlag); //画文字 + heightFlag += 29; + } + g1.dispose(); + g.dispose(); + FileOutputStream out = null; + try { + out = new FileOutputStream(outPath); //先用一个特定的输出文件名 + JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out); + JPEGEncodeParam param = encoder.getDefaultJPEGEncodeParam(bimage); + param.setQuality(100, true); // + encoder.encode(bimage, param); + } catch (Exception e) { + log.error("图片生成失败", e); + return false; + } finally { + try { + if(out != null) out.close(); + } catch (IOException e) { + log.error(e.getMessage(), e); + return false; + } + } + return true; + } + + + public static void gogo(){ + int fontSize = 14; + // 获取当前系统所有字体 + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + String[] fontNames = ge.getAvailableFontFamilyNames(); + String str = "转账批次号: 1231231"; + int height = 0; + int width = 0; + // 根据字体获取需要生成的图片的宽和高 + for (String fontName : fontNames) { + Font font = new Font(fontName, Font.PLAIN, fontSize); + FontDesignMetrics metrics = FontDesignMetrics.getMetrics(font); + height += metrics.getHeight(); + int tmpWidth = metrics.stringWidth(fontName + " : " + str); + if (tmpWidth > width) { + width = tmpWidth; + } + } + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); + Graphics2D g2d = null; + try { + //创建画笔 + g2d = image.createGraphics(); + //设置背景颜色为白色 + g2d.setColor(Color.WHITE); + g2d.fillRect(0, 0, image.getWidth(), image.getHeight()); + //设置画笔颜色为黑色 + g2d.setColor(Color.BLACK); + RenderingHints hints = new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + // 开启文字抗锯齿 + hints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g2d.setRenderingHints(hints); + int startY = 0; + for (String fontName : fontNames) { + Font font = new Font(fontName, Font.PLAIN, fontSize); + System.out.println(fontName); + g2d.setFont(font); + g2d.drawString(fontName + " : " + str, 0, startY); + // 下一行文字的左上角纵坐标 + FontDesignMetrics metrics = FontDesignMetrics.getMetrics(font); + startY += metrics.getHeight(); + } + String savePath = "d:\\Users\\004796\\桌面\\123\\1111.jpg"; + ImageIO.write(image, "PNG", new File(savePath)); + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (g2d != null) { + g2d.dispose(); + } + } + } + + +} +*/ diff --git a/bashi-dk/src/main/java/com/bashi/dk/util/Loan.java b/bashi-dk/src/main/java/com/bashi/dk/util/Loan.java new file mode 100644 index 0000000..3c1aab4 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/util/Loan.java @@ -0,0 +1,115 @@ +package com.bashi.dk.util; + +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; +import java.util.List; + +/** + * Created by WangGenshen on 1/14/16. + */ +public class Loan { + + private BigDecimal totalLoanMoney; //贷款总额 + private int totalMonth; //还款月份 + private double loanRate; //贷款年利率 + + @Getter + @Setter + private double loanRateMonth; + + private BigDecimal totalInterest; // 总利息数 + private BigDecimal totalRepayment; // 还款总额 + private BigDecimal firstRepayment; // 首月还款额 + private BigDecimal avgRepayment; // 月均还款额 + + private List allLoans; // 所有月份的还款情况 + + + + + public BigDecimal getTotalLoanMoney() { + return totalLoanMoney; + } + + public void setTotalLoanMoney(BigDecimal totalLoanMoney) { + this.totalLoanMoney = totalLoanMoney; + } + + public int getTotalMonth() { + return totalMonth; + } + + public void setTotalMonth(int totalMonth) { + this.totalMonth = totalMonth; + } + + public double getLoanRate() { + return loanRate; + } + + public void setLoanRate(double loanRate) { + this.loanRate = loanRate; + } + + public BigDecimal getTotalInterest() { + return totalInterest; + } + + public void setTotalInterest(BigDecimal totalInterest) { + this.totalInterest = totalInterest; + } + + public BigDecimal getTotalRepayment() { + return totalRepayment; + } + + public void setTotalRepayment(BigDecimal totalRepayment) { + this.totalRepayment = totalRepayment; + } + + public BigDecimal getFirstRepayment() { + return firstRepayment; + } + + public void setFirstRepayment(BigDecimal firstRepayment) { + this.firstRepayment = firstRepayment; + } + + public BigDecimal getAvgRepayment() { + return avgRepayment; + } + + public void setAvgRepayment(BigDecimal avgRepayment) { + this.avgRepayment = avgRepayment; + } + + public List getAllLoans() { + return allLoans; + } + + public void setAllLoans(List allLoans) { + this.allLoans = allLoans; + } + + @Override + public String toString() { + String allLoansStr = ""; + if (allLoans != null) { + for (LoanByMonth loanByMonth : allLoans) { + String lbmStr = "月份: " + loanByMonth.getMonth() + "\t第" + loanByMonth.getYear() + "年\t第" + + loanByMonth.getMonthInYear() + "月\t" + "月供: " + loanByMonth.getRepayment() + + "\t本金: " + loanByMonth.getPayPrincipal() + "\t利息: " + loanByMonth.getInterest() + + "\t剩余贷款: " + loanByMonth.getRemainTotal(); + if (allLoansStr.equals("")) { + allLoansStr = lbmStr; + } else { + allLoansStr += "\n" + lbmStr; + } + } + } + return "每月还款: " + getAvgRepayment() + "\t总利息: " + getTotalInterest() + + "\t还款总额:" + getTotalRepayment() + "\t首月还款: " + getFirstRepayment() + "\n" + allLoansStr; + } +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/util/LoanByMonth.java b/bashi-dk/src/main/java/com/bashi/dk/util/LoanByMonth.java new file mode 100644 index 0000000..c553b31 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/util/LoanByMonth.java @@ -0,0 +1,83 @@ +package com.bashi.dk.util; + +import java.math.BigDecimal; + +/** + * Created by WangGenshen on 1/14/16. + */ +public class LoanByMonth { + + private int month; // 第几个月份 + private BigDecimal repayment; // 该月还款额 + private BigDecimal payPrincipal; // 该月所还本金 + private BigDecimal interest; // 该月利息 + private BigDecimal remainTotal; // 剩余贷款 + private BigDecimal remainPrincipal; // 剩余总本金 + + private int year; // 第几年 + private int monthInYear; // 年里的第几月 + + public int getMonth() { + return month; + } + + public void setMonth(int month) { + this.month = month; + } + + public BigDecimal getRepayment() { + return repayment; + } + + public void setRepayment(BigDecimal repayment) { + this.repayment = repayment; + } + + public BigDecimal getPayPrincipal() { + return payPrincipal; + } + + public void setPayPrincipal(BigDecimal payPrincipal) { + this.payPrincipal = payPrincipal; + } + + public BigDecimal getInterest() { + return interest; + } + + public void setInterest(BigDecimal interest) { + this.interest = interest; + } + + public BigDecimal getRemainTotal() { + return remainTotal; + } + + public void setRemainTotal(BigDecimal remainTotal) { + this.remainTotal = remainTotal; + } + + public BigDecimal getRemainPrincipal() { + return remainPrincipal; + } + + public void setRemainPrincipal(BigDecimal remainPrincipal) { + this.remainPrincipal = remainPrincipal; + } + + public int getYear() { + return year; + } + + public void setYear(int year) { + this.year = year; + } + + public int getMonthInYear() { + return monthInYear; + } + + public void setMonthInYear(int monthInYear) { + this.monthInYear = monthInYear; + } +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/util/LoanCalculatorAdapter.java b/bashi-dk/src/main/java/com/bashi/dk/util/LoanCalculatorAdapter.java new file mode 100644 index 0000000..6a76cca --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/util/LoanCalculatorAdapter.java @@ -0,0 +1,17 @@ +package com.bashi.dk.util; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +/** + * Created by WangGenshen on 1/14/16. + */ +public class LoanCalculatorAdapter implements ILoanCalculator { + + @Override + public Loan calLoan(BigDecimal totalLoanMoney, int totalMonth, double loanRate, int rateType) { + return null; + } + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/util/LoanCalculatorTest.java b/bashi-dk/src/main/java/com/bashi/dk/util/LoanCalculatorTest.java new file mode 100644 index 0000000..5275cbb --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/util/LoanCalculatorTest.java @@ -0,0 +1,57 @@ +package com.bashi.dk.util; + +import org.xnio.channels.SuspendableAcceptChannel; + +import java.math.BigDecimal; + +/** + * Created by WangGenshen on 1/14/16. + */ +public class LoanCalculatorTest { + + private int totalMonth; + private BigDecimal totalMoney; + private double percent; + private double rate; + private double rateDiscount; + + protected void setUp() throws Exception { + totalMonth = 36; + totalMoney = new BigDecimal(50000); + percent = 0; + rate = 10.8; + rateDiscount = 1; + } + + public static void main(String[] args) { + ACPIMLoanCalculator calculator = new ACPIMLoanCalculator(); + Loan loan = calculator.calLoan( + LoanUtil.totalLoanMoney(new BigDecimal(50000), 2), + 36, + 0.6, + LoanUtil.RATE_TYPE_MONTH); + System.out.println("asd"); + } + + + public void testACPIMCalculate() { + ACPIMLoanCalculator calculator = new ACPIMLoanCalculator(); + Loan loan = calculator.calLoan( + LoanUtil.totalLoanMoney(totalMoney, percent), + totalMonth, + LoanUtil.rate(rate, rateDiscount), + LoanUtil.RATE_TYPE_YEAR); + System.out.println(loan); + } + + public void testACMCalculate() { + ACMLoanCalculator calculator = new ACMLoanCalculator(); + Loan loan = calculator.calLoan( + LoanUtil.totalLoanMoney(totalMoney, percent), + totalMonth, + LoanUtil.rate(rate, rateDiscount), + LoanUtil.RATE_TYPE_YEAR); + System.out.println(loan); + } + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/util/LoanUtil.java b/bashi-dk/src/main/java/com/bashi/dk/util/LoanUtil.java new file mode 100644 index 0000000..d84470e --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/util/LoanUtil.java @@ -0,0 +1,33 @@ +package com.bashi.dk.util; + +import java.math.BigDecimal; + +/** + * Created by WangGenshen on 1/23/16. + */ +public class LoanUtil { + + public static final int RATE_TYPE_YEAR = 10; + public static final int RATE_TYPE_MONTH = 11; + + public static BigDecimal totalMoney(double area, BigDecimal price, double discount) { + return price.multiply(new BigDecimal(area)).multiply(new BigDecimal(discount)).setScale(2, BigDecimal.ROUND_HALF_UP); + } + + public static BigDecimal totalLoanMoney(BigDecimal totalMoney, double percent) { + return totalMoney.multiply(new BigDecimal(1 - percent)).setScale(2, BigDecimal.ROUND_HALF_UP); + } + + public static BigDecimal totalLoanMoney(double area, BigDecimal price, double discount, double percent) { + return totalLoanMoney(totalMoney(area, price, discount), percent); + } + + public static double rate(double rate, double discount) { + return rate * discount; + } + + public static int totalMonth(int year) { + return 12 * year; + } + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/util/MoneyUtil.java b/bashi-dk/src/main/java/com/bashi/dk/util/MoneyUtil.java new file mode 100644 index 0000000..e02aff5 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/util/MoneyUtil.java @@ -0,0 +1,141 @@ +package com.bashi.dk.util; + +public class MoneyUtil { + + /** 大写数字 */ + private static final String[] NUMBERS = { "零", "壹", "贰", "叁", "肆", "伍", "陆", + "柒", "捌", "玖" }; + /** 整数部分的单位 */ + private static final String[] IUNIT = { "元", "拾", "佰", "仟", "万", "拾", "佰", + "仟", "亿", "拾", "佰", "仟", "万", "拾", "佰", "仟" }; + /** 小数部分的单位 */ + private static final String[] DUNIT = { "角", "分", "厘" }; + + /** + * 得到大写金额。 + */ + public static String toChinese(String str) { + str = str.replaceAll(",", "");// 去掉"," + String integerStr;// 整数部分数字 + String decimalStr;// 小数部分数字 + + // 初始化:分离整数部分和小数部分 + if (str.indexOf(".") > 0) { + integerStr = str.substring(0, str.indexOf(".")); + decimalStr = str.substring(str.indexOf(".") + 1); + } else if (str.indexOf(".") == 0) { + integerStr = ""; + decimalStr = str.substring(1); + } else { + integerStr = str; + decimalStr = ""; + } + // integerStr去掉首0,不必去掉decimalStr的尾0(超出部分舍去) + if (!integerStr.equals("")) { + integerStr = Long.toString(Long.parseLong(integerStr)); + if (integerStr.equals("0")) { + integerStr = ""; + } + } + // overflow超出处理能力,直接返回 + if (integerStr.length() > IUNIT.length) { + System.out.println(str + ":超出处理能力"); + return str; + } + + int[] integers = toArray(integerStr);// 整数部分数字 + boolean isMust5 = isMust5(integerStr);// 设置万单位 + int[] decimals = toArray(decimalStr);// 小数部分数字 + return getChineseInteger(integers, isMust5) + getChineseDecimal(decimals); + } + + /** + * 整数部分和小数部分转换为数组,从高位至低位 + */ + private static int[] toArray(String number) { + int[] array = new int[number.length()]; + for (int i = 0; i < number.length(); i++) { + array[i] = Integer.parseInt(number.substring(i, i + 1)); + } + return array; + } + + /** + * 得到中文金额的整数部分。 + */ + private static String getChineseInteger(int[] integers, boolean isMust5) { + StringBuffer chineseInteger = new StringBuffer(""); + int length = integers.length; + for (int i = 0; i < length; i++) { + // 0出现在关键位置:1234(万)5678(亿)9012(万)3456(元) + // 特殊情况:10(拾元、壹拾元、壹拾万元、拾万元) + String key = ""; + if (integers[i] == 0) { + if ((length - i) == 13)// 万(亿)(必填) + key = IUNIT[4]; + else if ((length - i) == 9)// 亿(必填) + key = IUNIT[8]; + else if ((length - i) == 5 && isMust5)// 万(不必填) + key = IUNIT[4]; + else if ((length - i) == 1)// 元(必填) + key = IUNIT[0]; + // 0遇非0时补零,不包含最后一位 + if ((length - i) > 1 && integers[i + 1] != 0) + key += NUMBERS[0]; + } + chineseInteger.append(integers[i] == 0 ? key + : (NUMBERS[integers[i]] + IUNIT[length - i - 1])); + } + return chineseInteger.toString(); + } + + /** + * 得到中文金额的小数部分。 + */ + private static String getChineseDecimal(int[] decimals) { + StringBuffer chineseDecimal = new StringBuffer(""); + for (int i = 0; i < decimals.length; i++) { + // 舍去3位小数之后的 + if (i == 3) + break; + chineseDecimal.append(decimals[i] == 0 ? "" + : (NUMBERS[decimals[i]] + DUNIT[i])); + } + return chineseDecimal.toString(); + } + + /** + * 判断第5位数字的单位"万"是否应加。 + */ + private static boolean isMust5(String integerStr) { + int length = integerStr.length(); + if (length > 4) { + String subInteger = ""; + if (length > 8) { + // 取得从低位数,第5到第8位的字串 + subInteger = integerStr.substring(length - 8, length - 4); + } else { + subInteger = integerStr.substring(0, length - 4); + } + return Integer.parseInt(subInteger) > 0; + } else { + return false; + } + } + + public static void main(String[] args) { + String number = "1.23"; + System.out.println(number + " " + MoneyUtil.toChinese(number)); + number = "1234567890123456.123"; + System.out.println(number + " " + MoneyUtil.toChinese(number)); + number = "0.0798"; + System.out.println(number + " " + MoneyUtil.toChinese(number)); + number = "10,001,000.09"; + System.out.println(number + " " + MoneyUtil.toChinese(number)); + number = "01.107700"; + System.out.println(number + " " + MoneyUtil.toChinese(number)); + number = "01.107700"; + System.out.println(number + " " + MoneyUtil.toChinese(number)); + } + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/util/OrderTradeNoUtil.java b/bashi-dk/src/main/java/com/bashi/dk/util/OrderTradeNoUtil.java new file mode 100644 index 0000000..1dca309 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/util/OrderTradeNoUtil.java @@ -0,0 +1,27 @@ +package com.bashi.dk.util; + +import com.baomidou.mybatisplus.core.toolkit.StringUtils; + +/** + *

created on 2021/8/23

+ * + * @author zhangliang + */ +public class OrderTradeNoUtil { + + public static final String BORROW = "B"; + + private static final SnowFlake snowFlake = new SnowFlake(2, 3); + + public static String createOrder(String orderType){ + return orderType + snowFlake.nextId(); + } + + public static String getOrderType(String tradeNo){ + if(StringUtils.isBlank(tradeNo)){ + return null; + } + return String.valueOf(tradeNo.charAt(0)); + } + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/util/PhoneRandomUtil.java b/bashi-dk/src/main/java/com/bashi/dk/util/PhoneRandomUtil.java new file mode 100644 index 0000000..8bdf4ca --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/util/PhoneRandomUtil.java @@ -0,0 +1,21 @@ +package com.bashi.dk.util; + +import java.util.Random; + +public class PhoneRandomUtil { + + public static String gen() { + Random random = new Random(); + StringBuilder phoneNumber = new StringBuilder(); + phoneNumber.append("1"); + for (int i = 0; i < 2; i++) { + phoneNumber.append(random.nextInt(10)); + } + phoneNumber.append("****"); + for (int i = 0; i < 4; i++) { + phoneNumber.append(random.nextInt(10)); + } + return phoneNumber.toString(); + } + +} diff --git a/bashi-dk/src/main/java/com/bashi/dk/util/SnowFlake.java b/bashi-dk/src/main/java/com/bashi/dk/util/SnowFlake.java new file mode 100644 index 0000000..8e393e9 --- /dev/null +++ b/bashi-dk/src/main/java/com/bashi/dk/util/SnowFlake.java @@ -0,0 +1,100 @@ +package com.bashi.dk.util; + +/** + * 描述: Twitter的分布式自增ID雪花算法snowflake (Java版) + * + * @author yanpenglei + * @create 2018-03-13 12:37 + **/ +public class SnowFlake { + + /** + * 起始的时间戳 + */ + private final static long START_STMP = 1480166465631L; + + /** + * 每一部分占用的位数 + */ + private final static long SEQUENCE_BIT = 12; //序列号占用的位数 + private final static long MACHINE_BIT = 5; //机器标识占用的位数 + private final static long DATACENTER_BIT = 5;//数据中心占用的位数 + + /** + * 每一部分的最大值 + */ + private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT); + private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT); + private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT); + + /** + * 每一部分向左的位移 + */ + private final static long MACHINE_LEFT = SEQUENCE_BIT; + private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT; + private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT; + + private long datacenterId; //数据中心 + private long machineId; //机器标识 + private long sequence = 0L; //序列号 + private long lastStmp = -1L;//上一次时间戳 + + public SnowFlake(long datacenterId, long machineId) { + if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) { + throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0"); + } + if (machineId > MAX_MACHINE_NUM || machineId < 0) { + throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0"); + } + this.datacenterId = datacenterId; + this.machineId = machineId; + } + + /** + * 产生下一个ID + * + * @return + */ + public synchronized long nextId() { + long currStmp = getNewstmp(); + if (currStmp < lastStmp) { + throw new RuntimeException("Clock moved backwards. Refusing to generate id"); + } + + if (currStmp == lastStmp) { + //相同毫秒内,序列号自增 + sequence = (sequence + 1) & MAX_SEQUENCE; + //同一毫秒的序列数已经达到最大 + if (sequence == 0L) { + currStmp = getNextMill(); + } + } else { + //不同毫秒内,序列号置为0 + sequence = 0L; + } + + lastStmp = currStmp; + + return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分 + | datacenterId << DATACENTER_LEFT //数据中心部分 + | machineId << MACHINE_LEFT //机器标识部分 + | sequence; //序列号部分 + } + + private long getNextMill() { + long mill = getNewstmp(); + while (mill <= lastStmp) { + mill = getNewstmp(); + } + return mill; + } + + private long getNewstmp() { + return System.currentTimeMillis(); + } + + public static void main(String[] args) { + + + } +} diff --git a/bashi-dk/src/main/resources/mapper/BorrowMapper.xml b/bashi-dk/src/main/resources/mapper/BorrowMapper.xml new file mode 100644 index 0000000..9fbb415 --- /dev/null +++ b/bashi-dk/src/main/resources/mapper/BorrowMapper.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/bashi-dk/src/main/resources/mapper/CustomerMapper.xml b/bashi-dk/src/main/resources/mapper/CustomerMapper.xml new file mode 100644 index 0000000..164983e --- /dev/null +++ b/bashi-dk/src/main/resources/mapper/CustomerMapper.xml @@ -0,0 +1,51 @@ + + + + + update dk_customer + set account = account + #{totalLoanMoney}, + borrow_account = #{totalLoanMoney}, + repayment_account = repayment_account + #{totalRepayment} + where id = #{customerId} + + + update dk_customer + set account = account - #{withdrawAmount} + where id = #{customerId} + + + + diff --git a/bashi-framework/pom.xml b/bashi-framework/pom.xml new file mode 100644 index 0000000..66d6f1c --- /dev/null +++ b/bashi-framework/pom.xml @@ -0,0 +1,91 @@ + + + + bashi + com.bashi + 2.4.0 + + 4.0.0 + + bashi-framework + + + framework框架核心 + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + spring-boot-starter-tomcat + org.springframework.boot + + + + + + org.springframework.boot + spring-boot-starter-websocket + + + + org.springframework.boot + spring-boot-starter-undertow + + + + + + + + + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + com.alibaba + druid-spring-boot-starter + + + com.sun + jconsole + + + com.sun + tools + + + + + + + com.bashi + bashi-system + + + com.bashi + bashi-common + + + + + diff --git a/bashi-framework/src/main/java/com/bashi/framework/app/AppTypeEnums.java b/bashi-framework/src/main/java/com/bashi/framework/app/AppTypeEnums.java new file mode 100644 index 0000000..438303a --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/app/AppTypeEnums.java @@ -0,0 +1,37 @@ +package com.bashi.framework.app; + +public enum AppTypeEnums { + + TBS("TBS","泰巴适"), + TATA("TATA","她他"), + MIYU("MIYU","秘约"), + APP591("591","591"), + MUSPAN("MUSPAN","幕Span"), + QIANZHI("QIANZHI","仟指"); + + private final String code; + private final String name; + + AppTypeEnums(String code, String name) { + this.code = code; + this.name = name; + } + + public String getName() { + return name; + } + + public String getCode() { + return code; + } + + public static AppTypeEnums getByCode(String code){ + AppTypeEnums[] values = AppTypeEnums.values(); + for (AppTypeEnums value : values) { + if(value.getCode().equals(code)){ + return value; + } + } + return AppTypeEnums.TBS; + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/app/AppTypeFactory.java b/bashi-framework/src/main/java/com/bashi/framework/app/AppTypeFactory.java new file mode 100644 index 0000000..189a4a9 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/app/AppTypeFactory.java @@ -0,0 +1,24 @@ +package com.bashi.framework.app; + +import com.bashi.common.utils.ServletUtils; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; + +@Component +public class AppTypeFactory { + + private static final String TYPE = "TYPE"; + + public String getAppType(){ + String type = getHeaderType(); + AppTypeEnums appTypeEnums = AppTypeEnums.getByCode(type); + return appTypeEnums.getCode(); + } + + public String getHeaderType(){ + HttpServletRequest request = ServletUtils.getRequest(); + return request.getHeader(TYPE); + } + +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/aspectj/DataScopeAspect.java b/bashi-framework/src/main/java/com/bashi/framework/aspectj/DataScopeAspect.java new file mode 100644 index 0000000..9d5e8e4 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/aspectj/DataScopeAspect.java @@ -0,0 +1,175 @@ +package com.bashi.framework.aspectj; + +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.StrUtil; +import com.bashi.common.annotation.DataScope; +import com.bashi.common.core.domain.BaseEntity; +import com.bashi.common.core.domain.entity.SysRole; +import com.bashi.common.core.domain.entity.SysUser; +import com.bashi.common.core.domain.model.LoginUser; +import com.bashi.common.utils.ServletUtils; +import com.bashi.common.utils.reflect.ReflectUtils; +import com.bashi.common.utils.spring.SpringUtils; +import com.bashi.framework.web.service.TokenService; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.Signature; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.Map; + +/** + * 数据过滤处理 + * + * @author Lion Li + */ +@Aspect +@Component +public class DataScopeAspect { + + /** + * 全部数据权限 + */ + public static final String DATA_SCOPE_ALL = "1"; + + /** + * 自定数据权限 + */ + public static final String DATA_SCOPE_CUSTOM = "2"; + + /** + * 部门数据权限 + */ + public static final String DATA_SCOPE_DEPT = "3"; + + /** + * 部门及以下数据权限 + */ + public static final String DATA_SCOPE_DEPT_AND_CHILD = "4"; + + /** + * 仅本人数据权限 + */ + public static final String DATA_SCOPE_SELF = "5"; + + /** + * 数据权限过滤关键字 + */ + public static final String DATA_SCOPE = "dataScope"; + + // 配置织入点 + @Pointcut("@annotation(com.bashi.common.annotation.DataScope)") + public void dataScopePointCut() { + } + + @Before("dataScopePointCut()") + public void doBefore(JoinPoint point) throws Throwable { + clearDataScope(point); + handleDataScope(point); + } + + protected void handleDataScope(final JoinPoint joinPoint) { + // 获得注解 + DataScope controllerDataScope = getAnnotationLog(joinPoint); + if (controllerDataScope == null) { + return; + } + // 获取当前的用户 + LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUser(ServletUtils.getRequest()); + if (Validator.isNotNull(loginUser)) { + SysUser currentUser = loginUser.getUser(); + // 如果是超级管理员,则不过滤数据 + if (Validator.isNotNull(currentUser) && !currentUser.isAdmin()) { + dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(), + controllerDataScope.userAlias(), controllerDataScope.isUser()); + } + } + } + + /** + * 数据范围过滤 + * + * @param joinPoint 切点 + * @param user 用户 + * @param userAlias 别名 + */ + public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias, boolean isUser) { + StringBuilder sqlString = new StringBuilder(); + + // 将 "." 提取出,不写别名为单表查询,写别名为多表查询 + deptAlias = StrUtil.isNotBlank(deptAlias) ? deptAlias + "." : ""; + userAlias = StrUtil.isNotBlank(userAlias) ? userAlias + "." : ""; + + for (SysRole role : user.getRoles()) { + String dataScope = role.getDataScope(); + if (DATA_SCOPE_ALL.equals(dataScope)) { + sqlString = new StringBuilder(); + break; + } else if (DATA_SCOPE_CUSTOM.equals(dataScope)) { + sqlString.append(StrUtil.format( + " OR {}dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", + deptAlias, role.getRoleId())); + } else if (DATA_SCOPE_DEPT.equals(dataScope)) { + sqlString.append(StrUtil.format(" OR {}dept_id = {} ", + deptAlias, user.getDeptId())); + } else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)) { + sqlString.append(StrUtil.format( + " OR {}dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )", + deptAlias, user.getDeptId(), user.getDeptId())); + } else if (DATA_SCOPE_SELF.equals(dataScope)) { + if (isUser) { + sqlString.append(StrUtil.format(" OR {}user_id = {} ", + userAlias, user.getUserId())); + } else { + // 数据权限为仅本人且没有userAlias别名不查询任何数据 + sqlString.append(" OR 1=0 "); + } + } + } + + if (StrUtil.isNotBlank(sqlString.toString())) { + putDataScope(joinPoint, sqlString.substring(4)); + } + } + + /** + * 是否存在注解,如果存在就获取 + */ + private DataScope getAnnotationLog(JoinPoint joinPoint) { + Signature signature = joinPoint.getSignature(); + MethodSignature methodSignature = (MethodSignature) signature; + Method method = methodSignature.getMethod(); + + if (method != null) { + return method.getAnnotation(DataScope.class); + } + return null; + } + + /** + * 拼接权限sql前先清空params.dataScope参数防止注入 + */ + private void clearDataScope(final JoinPoint joinPoint) { + Object params = joinPoint.getArgs()[0]; + if (Validator.isNotNull(params)) { + putDataScope(joinPoint, ""); + } + } + + private static void putDataScope(JoinPoint joinPoint, String sql) { + Object params = joinPoint.getArgs()[0]; + if (Validator.isNotNull(params)) { + if (params instanceof BaseEntity) { + BaseEntity baseEntity = (BaseEntity) params; + baseEntity.getParams().put(DATA_SCOPE, sql); + } else { + Map invoke = ReflectUtils.invokeGetter(params, "params"); + invoke.put(DATA_SCOPE, sql); + } + } + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/aspectj/DataSourceAspect.java b/bashi-framework/src/main/java/com/bashi/framework/aspectj/DataSourceAspect.java new file mode 100644 index 0000000..9caba2a --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/aspectj/DataSourceAspect.java @@ -0,0 +1,62 @@ +package com.bashi.framework.aspectj; + +import cn.hutool.core.lang.Validator; +import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder; +import com.bashi.common.annotation.DataSource; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +/** + * 多数据源处理 + * + * @author duteliang + */ +@Aspect +@Order(-500) +@Component +public class DataSourceAspect { + + @Pointcut("@annotation(com.bashi.common.annotation.DataSource)" + + "|| @within(com.bashi.common.annotation.DataSource)") + public void dsPointCut() { + } + + @Around("dsPointCut()") + public Object around(ProceedingJoinPoint point) throws Throwable { + DataSource dataSource = getDataSource(point); + + if (Validator.isNotNull(dataSource)) { + DynamicDataSourceContextHolder.poll(); + String source = dataSource.value().getSource(); + DynamicDataSourceContextHolder.push(source); + } + + try { + return point.proceed(); + } finally { + // 销毁数据源 在执行方法之后 + DynamicDataSourceContextHolder.clear(); + } + } + + /** + * 获取需要切换的数据源 + */ + public DataSource getDataSource(ProceedingJoinPoint point) { + MethodSignature signature = (MethodSignature) point.getSignature(); + DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class); + if (Objects.nonNull(dataSource)) { + return dataSource; + } + + return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class); + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/aspectj/LogAspect.java b/bashi-framework/src/main/java/com/bashi/framework/aspectj/LogAspect.java new file mode 100644 index 0000000..95e3ec5 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/aspectj/LogAspect.java @@ -0,0 +1,240 @@ +package com.bashi.framework.aspectj; + +import com.alibaba.fastjson.JSON; +import com.bashi.common.annotation.Log; +import com.bashi.common.core.domain.model.LoginUser; +import com.bashi.common.enums.BusinessStatus; +import com.bashi.common.utils.SecurityUtils; +import com.bashi.common.utils.ServletUtils; +import com.bashi.common.utils.spring.SpringUtils; +import com.bashi.framework.app.AppTypeFactory; +import com.bashi.framework.util.AgentUtils; +import com.bashi.framework.web.service.AsyncService; +import com.bashi.system.domain.SysOperLog; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpHeaders; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.validation.BindingResult; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Collection; +import java.util.Date; +import java.util.Map; +import java.util.Objects; + +/** + * 操作日志记录处理 + * + * @author nohi + */ +@Aspect +@Component +@Slf4j +public class LogAspect { + + @Autowired + private AppTypeFactory appTypeFactory; + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) { + handleLog(joinPoint, controllerLog, null, jsonResult); + } + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) { + handleLog(joinPoint, controllerLog, e, null); + } + + protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) { + try { + long startTime = System.currentTimeMillis(); + StringBuilder logString = new StringBuilder(); + logString.append("record logs:"); + // 获得注解 + if (controllerLog == null || (!controllerLog.isPrint() && !controllerLog.isSaveDb())) { + return; + } + // 获取当前的用户 + HttpServletRequest request = ServletUtils.getRequest(); + LoginUser loginUser = SecurityUtils.getLoginUserNoException(); + // *========数据库日志=========*// + SysOperLog operLog = new SysOperLog(); + operLog.setStatus(BusinessStatus.SUCCESS.ordinal()); + // 请求的地址 + String ip = ServletUtils.getClientIP(request); + operLog.setOperIp(ip); + operLog.setOperUrl(request.getRequestURI()); + // 设置请求方式 + operLog.setRequestMethod(request.getMethod()); + logString.append(String.format("ip=%s;",ip)); + if (loginUser != null) { + logString.append(String.format("userName=%s;",loginUser.getUsername())); + operLog.setOperName(loginUser.getUsername()); + } + String userAgent = request.getHeader(HttpHeaders.USER_AGENT); + logString.append(String.format("url=%s;method=%s;title=%s;",operLog.getOperUrl(),operLog.getRequestMethod(),controllerLog.title())); + logString.append(String.format("userAgent=%s;agentCode=%s;appHeader=%s;", + userAgent, + AgentUtils.getAgentCode(request),appTypeFactory.getHeaderType())); + if (e != null) { + operLog.setStatus(BusinessStatus.FAIL.ordinal()); + operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000)); + logString.append(String.format("exception=%s;",e.getMessage())); + } + // 设置方法名称 + String className = joinPoint.getTarget().getClass().getName(); + String methodName = joinPoint.getSignature().getName(); + operLog.setMethod(className + "." + methodName + "()"); + // 处理设置注解上的参数 + getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult,logString); + + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + logString.append("StartTime:").append(dateFormat.format(new Date(startTime))); + long endTime = System.currentTimeMillis(); + logString.append(";EndTime:").append(dateFormat.format(new Date(endTime))); + logString.append(";CostTime:").append(endTime - startTime).append("ms"); + AsyncService bean = SpringUtils.getBean(AsyncService.class); + if(controllerLog.isPrint()){ + bean.recordLog(logString); + } + if(controllerLog.isSaveDb()){ + bean.recordOper(operLog); + } + } catch (Exception exp) { + // 记录本地异常日志 + log.error("==前置通知异常=="); + log.error("异常信息:{}", exp.getMessage()); + exp.printStackTrace(); + } + } + + /** + * 获取注解中对方法的描述信息 用于Controller层注解 + * + * @param log 日志 + * @param operLog 操作日志 + * @throws Exception + */ + public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult, + StringBuilder logString) throws Exception { + // 设置action动作 + operLog.setBusinessType(log.businessType().ordinal()); + // 设置标题 + operLog.setTitle(log.title()); + // 设置操作人类别 + operLog.setOperatorType(log.operatorType().ordinal()); + // 是否需要保存request,参数和值 + if (log.isSaveRequestData()) { + // 获取参数的信息,传入到数据库中。 + setRequestValue(joinPoint, operLog,logString); + } + // 是否需要保存response,参数和值 + if (log.isSaveResponseData() && jsonResult != null) { + String jsonResultString = JSON.toJSONString(jsonResult); + operLog.setJsonResult(StringUtils.substring(jsonResultString, 0, 2000)); + logString.append(String.format("result=%s;",jsonResultString)); + } + } + + /** + * 获取请求的参数,放到log中 + * + * @param operLog 操作日志 + * @throws Exception 异常 + */ + private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog,StringBuilder logString) throws Exception { + String requestMethod = operLog.getRequestMethod(); + if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) { + String params = argsArrayToString(joinPoint.getArgs()); + operLog.setOperParam(StringUtils.substring(params, 0, 2000)); + logString.append(String.format("params=%s;",params)); + } else { + Map paramsMap = ServletUtils.getRequest().getParameterMap(); + String params = toMapString(paramsMap); + operLog.setOperParam(StringUtils.substring(params, 0, 2000)); + logString.append(String.format("params=%s;",params)); + } + } + + private String toMapString(Map paramsMap){ + StringBuilder paramString = new StringBuilder(); + paramString.append("{"); + for(Map.Entry paramEntry : paramsMap.entrySet()) { + paramString.append(paramEntry.getKey()).append(":"); + if (Objects.nonNull(paramEntry.getValue())) { + paramString.append(org.apache.commons.lang3.StringUtils.join(paramEntry.getValue(),",")); + } + paramString.append(";"); + } + paramString.append("}"); + return paramString.toString(); + } + + /** + * 参数拼装 + */ + private String argsArrayToString(Object[] paramsArray) { + String params = ""; + if (paramsArray != null && paramsArray.length > 0) { + for (Object o : paramsArray) { + if (o != null && !isFilterObject(o)) { + try { + Object jsonObj = JSON.toJSONString(o); + params += jsonObj + " "; + } catch (Exception e) { + } + } + } + } + return params.trim(); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param o 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + @SuppressWarnings("rawtypes") + public boolean isFilterObject(final Object o) { + Class clazz = o.getClass(); + if (clazz.isArray()) { + return clazz.getComponentType().isAssignableFrom(MultipartFile.class); + } else if (Collection.class.isAssignableFrom(clazz)) { + Collection collection = (Collection) o; + for (Object value : collection) { + return value instanceof MultipartFile; + } + } else if (Map.class.isAssignableFrom(clazz)) { + Map map = (Map) o; + for (Object value : map.entrySet()) { + Map.Entry entry = (Map.Entry) value; + return entry.getValue() instanceof MultipartFile; + } + } + return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse + || o instanceof BindingResult; + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/captcha/UnsignedMathGenerator.java b/bashi-framework/src/main/java/com/bashi/framework/captcha/UnsignedMathGenerator.java new file mode 100644 index 0000000..190c448 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/captcha/UnsignedMathGenerator.java @@ -0,0 +1,85 @@ +package com.bashi.framework.captcha; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.math.Calculator; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 无符号计算生成器 + * + * @author Lion Li + */ +public class UnsignedMathGenerator implements CodeGenerator { + + private static final long serialVersionUID = -5514819971774091076L; + + private static final String operators = "+-*"; + + /** + * 参与计算数字最大长度 + */ + private final int numberLength; + + /** + * 构造 + */ + public UnsignedMathGenerator() { + this(2); + } + + /** + * 构造 + * + * @param numberLength 参与计算最大数字位数 + */ + public UnsignedMathGenerator(int numberLength) { + this.numberLength = numberLength; + } + + @Override + public String generate() { + final int limit = getLimit(); + int min = RandomUtil.randomInt(limit); + int max = RandomUtil.randomInt(min, limit); + String number1 = Integer.toString(max); + String number2 = Integer.toString(min); + number1 = StrUtil.padAfter(number1, this.numberLength, CharUtil.SPACE); + number2 = StrUtil.padAfter(number2, this.numberLength, CharUtil.SPACE); + + return number1 + RandomUtil.randomChar(operators) + number2 + '='; + } + + @Override + public boolean verify(String code, String userInputCode) { + int result; + try { + result = Integer.parseInt(userInputCode); + } catch (NumberFormatException e) { + // 用户输入非数字 + return false; + } + + final int calculateResult = (int) Calculator.conversion(code); + return result == calculateResult; + } + + /** + * 获取验证码长度 + * + * @return 验证码长度 + */ + public int getLength() { + return this.numberLength * 2 + 2; + } + + /** + * 根据长度获取参与计算数字最大值 + * + * @return 最大值 + */ + private int getLimit() { + return Integer.parseInt("1" + StrUtil.repeat('0', this.numberLength)); + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/AdminServerConfig.java b/bashi-framework/src/main/java/com/bashi/framework/config/AdminServerConfig.java new file mode 100644 index 0000000..638fb46 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/AdminServerConfig.java @@ -0,0 +1,63 @@ +package com.bashi.framework.config; + +import de.codecentric.boot.admin.server.config.EnableAdminServer; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; +import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties; +import org.springframework.boot.task.TaskExecutorBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.thymeleaf.dialect.IDialect; +import org.thymeleaf.spring5.ISpringTemplateEngine; +import org.thymeleaf.spring5.SpringTemplateEngine; +import org.thymeleaf.templateresolver.ITemplateResolver; + +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.stream.Collectors; + +/** + * springboot-admin server配置类 + * + * @author Lion Li + */ +@Configuration +@EnableAdminServer +public class AdminServerConfig { + + @Lazy + @Bean(name = TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME) + @ConditionalOnMissingBean(Executor.class) + public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) { + return builder.build(); + } + + /** + * 解决 admin 与 项目 页面的交叉引用 将 admin 的路由放到最后 + * @param properties + * @param templateResolvers + * @param dialects + * @return + */ + @Bean + @ConditionalOnMissingBean(ISpringTemplateEngine.class) + SpringTemplateEngine templateEngine(ThymeleafProperties properties, + ObjectProvider templateResolvers, ObjectProvider dialects) { + SpringTemplateEngine engine = new SpringTemplateEngine(); + engine.setEnableSpringELCompiler(properties.isEnableSpringElCompiler()); + engine.setRenderHiddenMarkersBeforeCheckboxes(properties.isRenderHiddenMarkersBeforeCheckboxes()); + templateResolvers.orderedStream().forEach(engine::addTemplateResolver); + dialects.orderedStream().forEach(engine::addDialect); + Set templateResolvers1 = engine.getTemplateResolvers(); + templateResolvers1 = templateResolvers1.stream() + .sorted(Comparator.comparing(ITemplateResolver::getOrder)) + .collect(Collectors.toCollection(LinkedHashSet::new)); + engine.setTemplateResolvers(templateResolvers1); + return engine; + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/ApplicationConfig.java b/bashi-framework/src/main/java/com/bashi/framework/config/ApplicationConfig.java new file mode 100644 index 0000000..54e4759 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/ApplicationConfig.java @@ -0,0 +1,26 @@ +package com.bashi.framework.config; + +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +import java.util.TimeZone; + +/** + * 程序注解配置 + * + * @author Lion Li + */ +@Configuration +// 表示通过aop框架暴露该代理对象,AopContext能够访问 +@EnableAspectJAutoProxy(exposeProxy = true) +public class ApplicationConfig { + /** + * 时区配置 + */ + @Bean + public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization() { + return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault()); + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/AsyncConfig.java b/bashi-framework/src/main/java/com/bashi/framework/config/AsyncConfig.java new file mode 100644 index 0000000..30b8dc7 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/AsyncConfig.java @@ -0,0 +1,51 @@ +package com.bashi.framework.config; + +import com.bashi.common.exception.CustomException; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.AsyncConfigurerSupport; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.security.concurrent.DelegatingSecurityContextExecutorService; + +import java.util.Arrays; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; + +/** + * 异步配置 + * + * @author Lion Li + */ +@EnableAsync +@Configuration +public class AsyncConfig extends AsyncConfigurerSupport { + + @Autowired + @Qualifier("scheduledExecutorService") + private ScheduledExecutorService scheduledExecutorService; + + /** + * 异步执行需要使用权限框架自带的包装线程池 保证权限信息的传递 + */ + @Override + public Executor getAsyncExecutor() { + return new DelegatingSecurityContextExecutorService(scheduledExecutorService); + } + + /** + * 异步执行异常处理 + */ + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return (throwable, method, objects) -> { + throwable.printStackTrace(); + throw new CustomException( + "Exception message - " + throwable.getMessage() + + ", Method name - " + method.getName() + + ", Parameter value - " + Arrays.toString(objects)); + }; + } + +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/CaptchaConfig.java b/bashi-framework/src/main/java/com/bashi/framework/config/CaptchaConfig.java new file mode 100644 index 0000000..e2c48ee --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/CaptchaConfig.java @@ -0,0 +1,55 @@ +package com.bashi.framework.config; + +import java.awt.*; + +import cn.hutool.captcha.*; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 验证码配置 + * + * @author Lion Li + */ +@Configuration +public class CaptchaConfig { + + private final int width = 160; + private final int height = 60; + private final Color background = Color.PINK; + private final Font font = new Font("Arial", Font.BOLD, 48); + + /** + * 圆圈干扰验证码 + */ + @Bean(name = "CircleCaptcha") + public CircleCaptcha getCircleCaptcha() { + CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(width, height); + captcha.setBackground(background); + captcha.setFont(font); + return captcha; + } + + /** + * 线段干扰的验证码 + */ + @Bean(name = "LineCaptcha") + public LineCaptcha getLineCaptcha() { + LineCaptcha captcha = CaptchaUtil.createLineCaptcha(width, height); + captcha.setBackground(background); + captcha.setFont(font); + return captcha; + } + + /** + * 扭曲干扰验证码 + */ + @Bean(name = "ShearCaptcha") + public ShearCaptcha getShearCaptcha() { + ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(width, height); + captcha.setBackground(background); + captcha.setFont(font); + return captcha; + } + +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/DruidConfig.java b/bashi-framework/src/main/java/com/bashi/framework/config/DruidConfig.java new file mode 100644 index 0000000..3528625 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/DruidConfig.java @@ -0,0 +1,66 @@ +package com.bashi.framework.config; + +import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties; +import com.alibaba.druid.util.Utils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.servlet.*; +import java.io.IOException; + +/** + * druid 配置多数据源 + * + * @author duteliang + */ +@Configuration +public class DruidConfig { + + /** + * 去除监控页面底部的广告 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Bean + @ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true") + public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties) + { + // 获取web监控页面的参数 + DruidStatProperties.StatViewServlet config = properties.getStatViewServlet(); + // 提取common.js的配置路径 + String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*"; + String commonJsPattern = pattern.replaceAll("\\*", "js/common.js"); + final String filePath = "support/http/resources/js/common.js"; + // 创建filter进行过滤 + Filter filter = new Filter() + { + @Override + public void init(javax.servlet.FilterConfig filterConfig) throws ServletException + { + } + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException + { + chain.doFilter(request, response); + // 重置缓冲区,响应头不会被重置 +// response.resetBuffer(); + // 获取common.js + String text = Utils.readFromResource(filePath); + // 正则替换banner, 除去底部的广告信息 + text = text.replaceAll("
", ""); + text = text.replaceAll("powered.*?shrek.wang", ""); + response.getWriter().write(text); + } + @Override + public void destroy() + { + } + }; + FilterRegistrationBean registrationBean = new FilterRegistrationBean(); + registrationBean.setFilter(filter); + registrationBean.addUrlPatterns(commonJsPattern); + return registrationBean; + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/FeignConfig.java b/bashi-framework/src/main/java/com/bashi/framework/config/FeignConfig.java new file mode 100644 index 0000000..83c2342 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/FeignConfig.java @@ -0,0 +1,57 @@ +package com.bashi.framework.config; + +import feign.*; +import okhttp3.ConnectionPool; +import okhttp3.OkHttpClient; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.cloud.openfeign.FeignAutoConfiguration; +import org.springframework.cloud.openfeign.support.SpringMvcContract; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.TimeUnit; + +/** + * openfeign配置类 + * + * @author Lion Li + */ +@EnableFeignClients("${feign.package}") +@Configuration +@ConditionalOnClass(Feign.class) +@AutoConfigureBefore(FeignAutoConfiguration.class) +public class FeignConfig { + + @Bean + public OkHttpClient okHttpClient(){ + return new OkHttpClient.Builder() + .readTimeout(60, TimeUnit.SECONDS) + .connectTimeout(60, TimeUnit.SECONDS) + .writeTimeout(120, TimeUnit.SECONDS) + .connectionPool(new ConnectionPool()) + .build(); + } + + @Bean + public Contract feignContract() { + return new SpringMvcContract(); + } + + @Bean + public Logger.Level feignLoggerLevel() { + return Logger.Level.BASIC; + } + + @Bean + public Request.Options feignRequestOptions() { + return new Request.Options(10, TimeUnit.SECONDS, 60,TimeUnit.SECONDS,true); + } + + @Bean + public Retryer feignRetry() { + return new Retryer.Default(); + } + +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/FilterConfig.java b/bashi-framework/src/main/java/com/bashi/framework/config/FilterConfig.java new file mode 100644 index 0000000..c6388bf --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/FilterConfig.java @@ -0,0 +1,54 @@ +package com.bashi.framework.config; + +import cn.hutool.core.util.StrUtil; +import com.bashi.common.filter.RepeatableFilter; +import com.bashi.common.filter.XssFilter; +import com.bashi.framework.config.properties.XssProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.servlet.DispatcherType; +import java.util.HashMap; +import java.util.Map; + +/** + * Filter配置 + * + * @author Lion Li + */ +@Configuration +public class FilterConfig { + + @Autowired + private XssProperties xssProperties; + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Bean + public FilterRegistrationBean xssFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setDispatcherTypes(DispatcherType.REQUEST); + registration.setFilter(new XssFilter()); + registration.addUrlPatterns(StrUtil.splitToArray(xssProperties.getUrlPatterns(), ",")); + registration.setName("xssFilter"); + registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE); + Map initParameters = new HashMap(); + initParameters.put("excludes", xssProperties.getExcludes()); + initParameters.put("enabled", xssProperties.getEnabled()); + registration.setInitParameters(initParameters); + return registration; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Bean + public FilterRegistrationBean someFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setFilter(new RepeatableFilter()); + registration.addUrlPatterns("/*"); + registration.setName("repeatableFilter"); + registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE); + return registration; + } + +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/JacksonConfig.java b/bashi-framework/src/main/java/com/bashi/framework/config/JacksonConfig.java new file mode 100644 index 0000000..5543d0b --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/JacksonConfig.java @@ -0,0 +1,50 @@ +package com.bashi.framework.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.bashi.common.utils.JsonUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.LocalDateTime; +import java.util.TimeZone; + +/** + * jackson 配置 + * + * @author Lion Li + */ +@Slf4j +@Configuration +public class JacksonConfig { + + @Bean + public BeanPostProcessor objectMapperBeanPostProcessor() { + return new BeanPostProcessor() { + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (!(bean instanceof ObjectMapper)) { + return bean; + } + ObjectMapper objectMapper = (ObjectMapper) bean; + // 全局配置序列化返回 JSON 处理 + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(LocalDateTime.class, LocalDateTimeSerializer.INSTANCE); + simpleModule.addDeserializer(LocalDateTime.class, LocalDateTimeDeserializer.INSTANCE); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + objectMapper.setTimeZone(TimeZone.getDefault()); + JsonUtils.init(objectMapper); + log.info("初始化 jackson 配置"); + return bean; + } + }; + } + +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/MybatisPlusConfig.java b/bashi-framework/src/main/java/com/bashi/framework/config/MybatisPlusConfig.java new file mode 100644 index 0000000..0e50736 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/MybatisPlusConfig.java @@ -0,0 +1,108 @@ +package com.bashi.framework.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.baomidou.mybatisplus.core.injector.AbstractMethod; +import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector; +import com.baomidou.mybatisplus.core.injector.ISqlInjector; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import com.bashi.common.core.mybatisplus.methods.InsertAll; +import com.bashi.framework.mybatisplus.CreateAndUpdateMetaObjectHandler; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import java.util.List; + +/** + * mybatis-plus配置类 + * + * @author Lion Li + */ +@EnableTransactionManagement(proxyTargetClass = true) +@Configuration +// 指定要扫描的Mapper类的包的路径 +@MapperScan("${mybatis-plus.mapperPackage}") +public class MybatisPlusConfig { + + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + // 分页插件 + interceptor.addInnerInterceptor(paginationInnerInterceptor()); + // 乐观锁插件 + interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor()); + // 阻断插件 +// interceptor.addInnerInterceptor(blockAttackInnerInterceptor()); + return interceptor; + } + + /** + * 分页插件,自动识别数据库类型 + * https://baomidou.com/guide/interceptor-pagination.html + */ + public PaginationInnerInterceptor paginationInnerInterceptor() { + PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(); + // 设置数据库类型为mysql + paginationInnerInterceptor.setDbType(DbType.MYSQL); + // 设置最大单页限制数量,默认 500 条,-1 不受限制 + paginationInnerInterceptor.setMaxLimit(-1L); + return paginationInnerInterceptor; + } + + /** + * 乐观锁插件 + * https://baomidou.com/guide/interceptor-optimistic-locker.html + */ + public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor() { + return new OptimisticLockerInnerInterceptor(); + } + + /** + * 如果是对全表的删除或更新操作,就会终止该操作 + * https://baomidou.com/guide/interceptor-block-attack.html + */ +// public BlockAttackInnerInterceptor blockAttackInnerInterceptor() { +// return new BlockAttackInnerInterceptor(); +// } + + /** + * sql性能规范插件(垃圾SQL拦截) + * 如有需要可以启用 + */ +// public IllegalSQLInnerInterceptor illegalSQLInnerInterceptor() { +// return new IllegalSQLInnerInterceptor(); +// } + + + /** + * 元对象字段填充控制器 + * https://baomidou.com/guide/auto-fill-metainfo.html + */ + @Bean + public MetaObjectHandler metaObjectHandler() { + return new CreateAndUpdateMetaObjectHandler(); + } + + /** + * sql注入器配置 + * https://baomidou.com/guide/sql-injector.html + */ + @Bean + public ISqlInjector sqlInjector() { + return new DefaultSqlInjector() { + @Override + public List getMethodList(Class mapperClass, TableInfo tableInfo) { + List methodList = super.getMethodList(mapperClass,tableInfo); + methodList.add(new InsertAll()); + return methodList; + } + }; + } + + +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/RedisConfig.java b/bashi-framework/src/main/java/com/bashi/framework/config/RedisConfig.java new file mode 100644 index 0000000..f2733f3 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/RedisConfig.java @@ -0,0 +1,110 @@ +package com.bashi.framework.config; + +import cn.hutool.core.util.StrUtil; +import com.bashi.framework.config.properties.RedissonProperties; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.client.codec.Codec; +import org.redisson.codec.JsonJacksonCodec; +import org.redisson.config.Config; +import org.redisson.spring.cache.CacheConfig; +import org.redisson.spring.cache.RedissonSpringCacheManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; + +/** + * redis配置 + * + * @author Lion Li + */ +@Configuration +@EnableCaching +public class RedisConfig extends CachingConfigurerSupport { + + private static final String REDIS_PROTOCOL_PREFIX = "redis://"; + private static final String REDISS_PROTOCOL_PREFIX = "rediss://"; + + @Autowired + private RedisProperties redisProperties; + + @Autowired + private RedissonProperties redissonProperties; + + @Bean(destroyMethod = "shutdown") + @ConditionalOnMissingBean(RedissonClient.class) + public RedissonClient redisson() throws IOException { + String prefix = REDIS_PROTOCOL_PREFIX; + if (redisProperties.isSsl()) { + prefix = REDISS_PROTOCOL_PREFIX; + } + Config config = new Config(); + config.setThreads(redissonProperties.getThreads()) + .setNettyThreads(redissonProperties.getNettyThreads()) + .setCodec(JsonJacksonCodec.INSTANCE) + .setTransportMode(redissonProperties.getTransportMode()); + + RedissonProperties.SingleServerConfig singleServerConfig = redissonProperties.getSingleServerConfig(); + ObjectMapper om = new ObjectMapper(); + om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + // 解决jackson2无法反序列化LocalDateTime的问题 + om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + JavaTimeModule timeModule = new JavaTimeModule(); + timeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); + timeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + timeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); + timeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + om.registerModule(timeModule); + config.setCodec(new JsonJacksonCodec(om)); + // 使用单机模式 + config.useSingleServer() + .setAddress(prefix + redisProperties.getHost() + ":" + redisProperties.getPort()) + .setConnectTimeout(((Long) redisProperties.getTimeout().toMillis()).intValue()) + .setDatabase(redisProperties.getDatabase()) + .setPassword(StrUtil.isNotBlank(redisProperties.getPassword()) ? redisProperties.getPassword() : null) + .setTimeout(singleServerConfig.getTimeout()) + .setRetryAttempts(singleServerConfig.getRetryAttempts()) + .setRetryInterval(singleServerConfig.getRetryInterval()) + .setSubscriptionsPerConnection(singleServerConfig.getSubscriptionsPerConnection()) + .setClientName(singleServerConfig.getClientName()) + .setIdleConnectionTimeout(singleServerConfig.getIdleConnectionTimeout()) + .setSubscriptionConnectionMinimumIdleSize(singleServerConfig.getSubscriptionConnectionMinimumIdleSize()) + .setSubscriptionConnectionPoolSize(singleServerConfig.getSubscriptionConnectionPoolSize()) + .setConnectionMinimumIdleSize(singleServerConfig.getConnectionMinimumIdleSize()) + .setConnectionPoolSize(singleServerConfig.getConnectionPoolSize()) + .setDnsMonitoringInterval(singleServerConfig.getDnsMonitoringInterval()); + return Redisson.create(config); + } + + /** + * 整合spring-cache + */ + @Bean + public CacheManager cacheManager(RedissonClient redissonClient) { + Map config = new HashMap<>(); + config.put("redissonCacheMap", new CacheConfig(30*60*1000, 10*60*1000)); + return new RedissonSpringCacheManager(redissonClient, config, JsonJacksonCodec.INSTANCE); + } + +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/ResourcesConfig.java b/bashi-framework/src/main/java/com/bashi/framework/config/ResourcesConfig.java new file mode 100644 index 0000000..62af1b3 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/ResourcesConfig.java @@ -0,0 +1,62 @@ +package com.bashi.framework.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import com.bashi.common.config.BsConfig; +import com.bashi.common.constant.Constants; +import com.bashi.framework.interceptor.RepeatSubmitInterceptor; + +/** + * 通用配置 + * + * @author duteliang + */ +@Configuration +public class ResourcesConfig implements WebMvcConfigurer +{ + @Autowired + private RepeatSubmitInterceptor repeatSubmitInterceptor; + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) + { + /** 本地文件上传路径 */ + registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**").addResourceLocations("file:" + BsConfig.getProfile() + "/"); + } + + /** + * 自定义拦截规则 + */ + @Override + public void addInterceptors(InterceptorRegistry registry) + { + registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); + } + + /** + * 跨域配置 + */ + @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); + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/SecurityConfig.java b/bashi-framework/src/main/java/com/bashi/framework/config/SecurityConfig.java new file mode 100644 index 0000000..6c60c1a --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/SecurityConfig.java @@ -0,0 +1,172 @@ +package com.bashi.framework.config; + +import com.bashi.framework.security.filter.JwtAuthenticationTokenFilter; +import com.bashi.framework.security.handle.AuthenticationEntryPointImpl; +import com.bashi.framework.security.handle.LogoutSuccessHandlerImpl; +import de.codecentric.boot.admin.server.config.AdminServerProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +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.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.web.filter.CorsFilter; + +import javax.annotation.Resource; + +/** + * spring security配置 + * + * @author duteliang + */ +@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) +@EnableWebSecurity(debug = false) +public class SecurityConfig extends WebSecurityConfigurerAdapter +{ + /** + * 自定义用户认证逻辑 + */ + @Resource + private UserDetailsService userDetailsService; + + /** + * 认证失败处理类 + */ + @Autowired + private AuthenticationEntryPointImpl unauthorizedHandler; + + /** + * 退出处理类 + */ + @Autowired + private LogoutSuccessHandlerImpl logoutSuccessHandler; + + /** + * token认证过滤器 + */ + @Autowired + private JwtAuthenticationTokenFilter authenticationTokenFilter; + + /** + * 跨域过滤器 + */ + @Autowired + private CorsFilter corsFilter; + + @Autowired + private AdminServerProperties adminServerProperties; + + @Autowired + private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig; + + /** + * 解决 无法直接注入 AuthenticationManager + * + * @return + * @throws Exception + */ + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception + { + return super.authenticationManagerBean(); + } + + /** + * anyRequest | 匹配所有请求路径 + * access | SpringEl表达式结果为true时可以访问 + * anonymous | 匿名可以访问 + * denyAll | 用户不能访问 + * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) + * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 + * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问 + * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 + * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 + * hasRole | 如果有参数,参数表示角色,则其角色可以访问 + * permitAll | 用户可以任意访问 + * rememberMe | 允许通过remember-me登录的用户访问 + * authenticated | 用户登录后可访问 + */ + @Override + protected void configure(HttpSecurity httpSecurity) throws Exception + { + httpSecurity + // CSRF禁用,因为不使用session + .csrf().disable() + // 认证失败处理类 + .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() + // 基于token,所以不需要session + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() + // 过滤请求 + .authorizeRequests() + // 对于登录login 验证码captchaImage 允许匿名访问 + .antMatchers("/login", "/captchaImage", + "/customer/login","/customer/open/**" + ).anonymous() + .antMatchers( + HttpMethod.GET, + "/*.html", + "/**/*.html", + "/**/*.css", + "/download/**", + "/**/*.js" + ).permitAll() + .antMatchers("/ws/**").permitAll() + .antMatchers("/open/common/upload").permitAll() + .antMatchers("/app/home/**").permitAll() + .antMatchers("/pay/notify/**").permitAll() + .antMatchers("/profile/**").anonymous() + .antMatchers("/common/download**").anonymous() + .antMatchers("/common/download/resource**").anonymous() + .antMatchers("/doc.html").anonymous() + .antMatchers("/swagger-resources/**").anonymous() + .antMatchers("/webjars/**").anonymous() + .antMatchers("/*/api-docs").anonymous() +// .antMatchers("/druid/**").anonymous() + // Spring Boot Admin Server 的安全配置 + .antMatchers(adminServerProperties.getContextPath()).anonymous() + .antMatchers(adminServerProperties.getContextPath() + "/**").anonymous() + // Spring Boot Actuator 的安全配置 +// .antMatchers("/actuator").anonymous() +// .antMatchers("/actuator/**").anonymous() + // 除上面外的所有请求全部需要鉴权认证 + .anyRequest().authenticated() + .and() + .headers().frameOptions().disable() + .and() + .apply(smsCodeAuthenticationSecurityConfig); + httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); + // 添加JWT filter + httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); + // 添加CORS filter + httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class); + httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class); + } + + + /** + * 强散列哈希加密实现 + */ + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() + { + return new BCryptPasswordEncoder(); + } + + /** + * 身份认证接口 + */ + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception + { + auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/ServerConfig.java b/bashi-framework/src/main/java/com/bashi/framework/config/ServerConfig.java new file mode 100644 index 0000000..e0f115d --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/ServerConfig.java @@ -0,0 +1,33 @@ +package com.bashi.framework.config; + +import javax.servlet.http.HttpServletRequest; + +import com.bashi.common.utils.ServletUtils; +import org.springframework.stereotype.Component; + +/** + * 服务相关配置 + * + * @author duteliang + */ +@Component +public class ServerConfig +{ + /** + * 获取完整的请求路径,包括:域名,端口,上下文访问路径 + * + * @return 服务地址 + */ + public String getUrl() + { + HttpServletRequest request = ServletUtils.getRequest(); + return getDomain(request); + } + + public static String getDomain(HttpServletRequest request) + { + StringBuffer url = request.getRequestURL(); + String contextPath = request.getServletContext().getContextPath(); + return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString(); + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/SmsCodeAuthenticationSecurityConfig.java b/bashi-framework/src/main/java/com/bashi/framework/config/SmsCodeAuthenticationSecurityConfig.java new file mode 100644 index 0000000..70ec9a4 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/SmsCodeAuthenticationSecurityConfig.java @@ -0,0 +1,47 @@ +package com.bashi.framework.config; + +import com.bashi.framework.security.sms.*; +import com.bashi.framework.web.service.CodeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +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; +import org.springframework.stereotype.Component; + +/** + *

created on 2021/7/13

+ * + * @author zhangliang + */ +@Component +public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter { + + @Autowired + private IDuteUserDetailsService userDetailsService; + @Autowired + private CodeService codeService; + @Autowired + private SmsAuthenticationSuccessHandler smsAuthenticationSuccessHandler; + @Autowired + private SmsAuthenticationFailureHandler smsAuthenticationFailureHandler; + + @Override + public void configure(HttpSecurity http) { + // 手机号登陆 + SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter(); + smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); + smsAuthenticationFilter.setAuthenticationSuccessHandler(smsAuthenticationSuccessHandler); + smsAuthenticationFilter.setAuthenticationFailureHandler(smsAuthenticationFailureHandler); + smsAuthenticationFilter.setCodeService(codeService); + + SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider(); + smsAuthenticationProvider.setUserDetailsService(userDetailsService); + smsAuthenticationProvider.setCodeService(codeService); + + http.authenticationProvider(smsAuthenticationProvider) + .addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + } + +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/SwaggerConfig.java b/bashi-framework/src/main/java/com/bashi/framework/config/SwaggerConfig.java new file mode 100644 index 0000000..a5f60eb --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/SwaggerConfig.java @@ -0,0 +1,108 @@ +package com.bashi.framework.config; + +import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j; +import com.bashi.framework.config.properties.SwaggerProperties; +import io.swagger.annotations.Api; +import io.swagger.models.auth.In; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.*; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.contexts.SecurityContext; +import springfox.documentation.spring.web.plugins.Docket; + +import java.util.ArrayList; +import java.util.List; + +/** + * Swagger 文档配置 + * + * @author Lion Li + */ +@Configuration +@EnableKnife4j +public class SwaggerConfig { + + @Autowired + private SwaggerProperties swaggerProperties; + + /** + * 创建API + */ + @Bean + public Docket createRestApi() { + return new Docket(DocumentationType.OAS_30) + .enable(swaggerProperties.getEnabled()) + // 用来创建该API的基本信息,展示在文档的页面中(自定义展示的信息) + .apiInfo(apiInfo()) + // 设置哪些接口暴露给Swagger展示 + .select() + // 扫描所有有注解的api,用这种方式更灵活 + .apis(RequestHandlerSelectors.withClassAnnotation(Api.class)) + // 扫描指定包中的swagger注解 + // .apis(RequestHandlerSelectors.basePackage("com.bashi.project.tool.swagger")) + // 扫描所有 .apis(RequestHandlerSelectors.any()) + .paths(PathSelectors.any()) + .build() + /* 设置安全模式,swagger可以设置访问token */ + .securitySchemes(securitySchemes()) + .securityContexts(securityContexts()) + .pathMapping(swaggerProperties.getPathMapping()); + } + + /** + * 安全模式,这里指定token通过Authorization头请求头传递 + */ + private List securitySchemes() { + List apiKeyList = new ArrayList(); + apiKeyList.add(new ApiKey("Authorization", "Authorization", In.HEADER.toValue())); + return apiKeyList; + } + + /** + * 安全上下文 + */ + private List securityContexts() { + List securityContexts = new ArrayList<>(); + securityContexts.add( + SecurityContext.builder() + .securityReferences(defaultAuth()) + .operationSelector(o -> o.requestMappingPattern().matches("/.*")) + .build()); + return securityContexts; + } + + /** + * 默认的安全上引用 + */ + private List defaultAuth() { + AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything"); + AuthorizationScope[] authorizationScopes = new AuthorizationScope[1]; + authorizationScopes[0] = authorizationScope; + List securityReferences = new ArrayList<>(); + securityReferences.add(new SecurityReference("Authorization", authorizationScopes)); + return securityReferences; + } + + /** + * 添加摘要信息 + */ + private ApiInfo apiInfo() { + // 用ApiInfoBuilder进行定制 + SwaggerProperties.Contact contact = swaggerProperties.getContact(); + return new ApiInfoBuilder() + // 设置标题 + .title(swaggerProperties.getTitle()) + // 描述 + .description(swaggerProperties.getDescription()) + // 作者信息 + .contact(new Contact(contact.getName(), contact.getUrl(), contact.getEmail())) + // 版本 + .version(swaggerProperties.getVersion()) + .build(); + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/ThreadPoolConfig.java b/bashi-framework/src/main/java/com/bashi/framework/config/ThreadPoolConfig.java new file mode 100644 index 0000000..765576b --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/ThreadPoolConfig.java @@ -0,0 +1,70 @@ +package com.bashi.framework.config; + +import com.bashi.common.utils.Threads; +import com.bashi.framework.config.properties.ThreadPoolProperties; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 线程池配置 + * + * @author Lion Li + **/ +@Configuration +public class ThreadPoolConfig { + + @Autowired + private ThreadPoolProperties threadPoolProperties; + + @Bean("threadPoolTaskExecutor") + @ConditionalOnProperty(prefix = "threadPoolTaskExecutor", name = "enabled", havingValue = "true") + public ThreadPoolTaskExecutor threadPoolTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setMaxPoolSize(threadPoolProperties.getMaxPoolSize()); + executor.setCorePoolSize(threadPoolProperties.getCorePoolSize()); + executor.setQueueCapacity(threadPoolProperties.getQueueCapacity()); + executor.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds()); + executor.setThreadNamePrefix("new-591-"); + RejectedExecutionHandler handler; + switch (threadPoolProperties.getRejectedExecutionHandler()) { + case "CallerRunsPolicy": + handler = new ThreadPoolExecutor.CallerRunsPolicy(); + break; + case "DiscardOldestPolicy": + handler = new ThreadPoolExecutor.DiscardOldestPolicy(); + break; + case "DiscardPolicy": + handler = new ThreadPoolExecutor.DiscardPolicy(); + break; + default: + handler = new ThreadPoolExecutor.AbortPolicy(); + break; + } + executor.setRejectedExecutionHandler(handler); + return executor; + } + + /** + * 执行周期性或定时任务 + */ + @Bean(name = "scheduledExecutorService") + protected ScheduledExecutorService scheduledExecutorService() { + return new ScheduledThreadPoolExecutor(threadPoolProperties.getCorePoolSize(), + new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build()) { + @Override + protected void afterExecute(Runnable r, Throwable t) { + super.afterExecute(r, t); + Threads.printException(r, t); + } + }; + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/ValidatorConfig.java b/bashi-framework/src/main/java/com/bashi/framework/config/ValidatorConfig.java new file mode 100644 index 0000000..8573cca --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/ValidatorConfig.java @@ -0,0 +1,31 @@ +package com.bashi.framework.config; + +import org.hibernate.validator.HibernateValidator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; + +/** + * 校验框架配置类 + * + * @author Lion Li + */ +@Configuration +public class ValidatorConfig { + + /** + * 配置校验框架 快速返回模式 + */ + @Bean + public Validator validator() { + ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) + .buildValidatorFactory(); + return validatorFactory.getValidator(); + } + +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/WebSocketConfig.java b/bashi-framework/src/main/java/com/bashi/framework/config/WebSocketConfig.java new file mode 100644 index 0000000..6ebcd31 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/WebSocketConfig.java @@ -0,0 +1,14 @@ +package com.bashi.framework.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.server.standard.ServerEndpointExporter; + +@Configuration +public class WebSocketConfig { + + @Bean + public ServerEndpointExporter serverEndpointExporter(){ + return new ServerEndpointExporter(); + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/WebSocketServer.java b/bashi-framework/src/main/java/com/bashi/framework/config/WebSocketServer.java new file mode 100644 index 0000000..4a2b895 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/WebSocketServer.java @@ -0,0 +1,146 @@ +package com.bashi.framework.config; + +import com.bashi.common.utils.JsonUtils; +import com.bashi.framework.dto.WebSocketMessageDTO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.websocket.OnClose; +import javax.websocket.OnError; +import javax.websocket.OnOpen; +import javax.websocket.Session; +import javax.websocket.server.PathParam; +import javax.websocket.server.ServerEndpoint; +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; + +@ServerEndpoint("/ws/{uuid}") +@Component +@Slf4j +public class WebSocketServer { + + /**静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。*/ + private static int onlineCount = 0; + /**concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。*/ + private static ConcurrentHashMap webSocketMap = new ConcurrentHashMap<>(); + /**与某个客户端的连接会话,需要通过它来给客户端发送数据*/ + private Session session; + /**接收userId*/ + private String uuidKey =""; + + /** + * 连接建立成功调用的方法*/ + @OnOpen + public void onOpen(Session session,@PathParam("uuid") String uuid) { + this.session = session; + this.uuidKey =uuid; + if(webSocketMap.containsKey(uuid)){ + webSocketMap.remove(uuid); + webSocketMap.put(uuid,this); + //加入set中 + }else{ + webSocketMap.put(uuid,this); + //加入set中 + addOnlineCount(); + //在线数加1 + } + + log.info("用户连接:"+uuid+",当前在线人数为:" + getOnlineCount()); + + try { + sendMessage("连接成功"); + } catch (IOException e) { + log.error("用户:"+uuid+",网络异常!!!!!!"); + } + } + + /** + * 连接关闭调用的方法 + */ + @OnClose + public void onClose() { + if(webSocketMap.containsKey(uuidKey)){ + webSocketMap.remove(uuidKey); + //从set中删除 + subOnlineCount(); + } + log.info("用户退出:"+ uuidKey +",当前在线人数为:" + getOnlineCount()); + } + +// /** +// * 收到客户端消息后调用的方法 +// * +// * @param message 客户端发送过来的消息*/ +// @OnMessage +// public void onMessage(String message, Session session) { +// log.info("用户消息:"+userId+",报文:"+message); +// //可以群发消息 +// //消息保存到数据库、redis +// if(StringUtils.isNotBlank(message)){ +// try { +// //解析发送的报文 +// JSONObject jsonObject = JSON.parseObject(message); +// //追加发送人(防止串改) +// jsonObject.put("fromUserId",this.userId); +// String toUserId=jsonObject.getString("toUserId"); +// //传送给对应toUserId用户的websocket +// if(StringUtils.isNotBlank(toUserId)&&webSocketMap.containsKey(toUserId)){ +// webSocketMap.get(toUserId).sendMessage(jsonObject.toJSONString()); +// }else{ +// log.error("请求的userId:"+toUserId+"不在该服务器上"); +// //否则不在这个服务器上,发送到mysql或者redis +// } +// }catch (Exception e){ +// e.printStackTrace(); +// } +// } +// } + + /** + * + * @param session + * @param error + */ + @OnError + public void onError(Session session, Throwable error) { + log.error("用户错误:"+this.uuidKey +",原因:"+error.getMessage()); + error.printStackTrace(); + } + + /** + * 实现服务器主动推送 + */ + public void sendMessage(String message) throws IOException { + this.session.getBasicRemote().sendText(message); + } + + /** + * 发送自定义消息 + * */ + /*public static void sendInfo(String message,@PathParam("userId") String userId) throws IOException { + log.info("发送消息到:"+userId+",报文:"+message); + if(StringUtils.isNotBlank(userId)&&webSocketMap.containsKey(userId)){ + webSocketMap.get(userId).sendMessage(message); + }else{ + log.error("用户"+userId+",不在线!"); + } + }*/ + + public static void sendInfoAll(WebSocketMessageDTO message) throws IOException { + for (WebSocketServer value : webSocketMap.values()) { + value.sendMessage(JsonUtils.toJsonString(message)); + } + } + + public static synchronized int getOnlineCount() { + return onlineCount; + } + + public static synchronized void addOnlineCount() { + WebSocketServer.onlineCount++; + } + + public static synchronized void subOnlineCount() { + WebSocketServer.onlineCount--; + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/WebSocketServerDemo.java b/bashi-framework/src/main/java/com/bashi/framework/config/WebSocketServerDemo.java new file mode 100644 index 0000000..5093ce2 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/WebSocketServerDemo.java @@ -0,0 +1,143 @@ +package com.bashi.framework.config; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import javax.websocket.OnClose; +import javax.websocket.OnError; +import javax.websocket.OnOpen; +import javax.websocket.Session; +import javax.websocket.server.PathParam; +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; + +//@ServerEndpoint("/ws/{userId}") +//@Component +@Slf4j +public class WebSocketServerDemo { + + /**静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。*/ + private static int onlineCount = 0; + /**concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。*/ + private static ConcurrentHashMap webSocketMap = new ConcurrentHashMap<>(); + /**与某个客户端的连接会话,需要通过它来给客户端发送数据*/ + private Session session; + /**接收userId*/ + private String userId=""; + + /** + * 连接建立成功调用的方法*/ + @OnOpen + public void onOpen(Session session,@PathParam("userId") String userId) { + this.session = session; + this.userId=userId; + if(webSocketMap.containsKey(userId)){ + webSocketMap.remove(userId); + webSocketMap.put(userId,this); + //加入set中 + }else{ + webSocketMap.put(userId,this); + //加入set中 + addOnlineCount(); + //在线数加1 + } + + log.info("用户连接:"+userId+",当前在线人数为:" + getOnlineCount()); + + try { + sendMessage("连接成功"); + } catch (IOException e) { + log.error("用户:"+userId+",网络异常!!!!!!"); + } + } + + /** + * 连接关闭调用的方法 + */ + @OnClose + public void onClose() { + if(webSocketMap.containsKey(userId)){ + webSocketMap.remove(userId); + //从set中删除 + subOnlineCount(); + } + log.info("用户退出:"+userId+",当前在线人数为:" + getOnlineCount()); + } + +// /** +// * 收到客户端消息后调用的方法 +// * +// * @param message 客户端发送过来的消息*/ +// @OnMessage +// public void onMessage(String message, Session session) { +// log.info("用户消息:"+userId+",报文:"+message); +// //可以群发消息 +// //消息保存到数据库、redis +// if(StringUtils.isNotBlank(message)){ +// try { +// //解析发送的报文 +// JSONObject jsonObject = JSON.parseObject(message); +// //追加发送人(防止串改) +// jsonObject.put("fromUserId",this.userId); +// String toUserId=jsonObject.getString("toUserId"); +// //传送给对应toUserId用户的websocket +// if(StringUtils.isNotBlank(toUserId)&&webSocketMap.containsKey(toUserId)){ +// webSocketMap.get(toUserId).sendMessage(jsonObject.toJSONString()); +// }else{ +// log.error("请求的userId:"+toUserId+"不在该服务器上"); +// //否则不在这个服务器上,发送到mysql或者redis +// } +// }catch (Exception e){ +// e.printStackTrace(); +// } +// } +// } + + /** + * + * @param session + * @param error + */ + @OnError + public void onError(Session session, Throwable error) { + log.error("用户错误:"+this.userId+",原因:"+error.getMessage()); + error.printStackTrace(); + } + + /** + * 实现服务器主动推送 + */ + public void sendMessage(String message) throws IOException { + this.session.getBasicRemote().sendText(message); + } + + /** + * 发送自定义消息 + * */ + public static void sendInfo(String message,@PathParam("userId") String userId) throws IOException { + log.info("发送消息到:"+userId+",报文:"+message); + if(StringUtils.isNotBlank(userId)&&webSocketMap.containsKey(userId)){ + webSocketMap.get(userId).sendMessage(message); + }else{ + log.error("用户"+userId+",不在线!"); + } + } + + public static void sendInfoAll(String message) throws IOException { + for (WebSocketServerDemo value : webSocketMap.values()) { + value.sendMessage(message); + } + } + + public static synchronized int getOnlineCount() { + return onlineCount; + } + + public static synchronized void addOnlineCount() { + WebSocketServerDemo.onlineCount++; + } + + public static synchronized void subOnlineCount() { + WebSocketServerDemo.onlineCount--; + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/properties/CaptchaProperties.java b/bashi-framework/src/main/java/com/bashi/framework/config/properties/CaptchaProperties.java new file mode 100644 index 0000000..404b657 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/properties/CaptchaProperties.java @@ -0,0 +1,41 @@ +package com.bashi.framework.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 验证码 配置属性 + * + * @author Lion Li + */ +@Data +@Component +@ConfigurationProperties(prefix = "captcha") +public class CaptchaProperties { + + /** + * 验证码开关 + */ + private Boolean enabled; + + /** + * 验证码类型 + */ + private String type; + + /** + * 验证码类别 + */ + private String category; + + /** + * 数字验证码位数 + */ + private Integer numberLength; + + /** + * 字符验证码长度 + */ + private Integer charLength; +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/properties/RedissonProperties.java b/bashi-framework/src/main/java/com/bashi/framework/config/properties/RedissonProperties.java new file mode 100644 index 0000000..484017c --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/properties/RedissonProperties.java @@ -0,0 +1,100 @@ +package com.bashi.framework.config.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.redisson.config.TransportMode; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * Redisson 配置属性 + * + * @author Lion Li + */ +@Data +@Component +@ConfigurationProperties(prefix = "redisson") +public class RedissonProperties { + + /** + * 线程池数量,默认值 = 当前处理核数量 * 2 + */ + private int threads; + + /** + * Netty线程池数量,默认值 = 当前处理核数量 * 2 + */ + private int nettyThreads; + + /** + * 传输模式 + */ + private TransportMode transportMode; + + /** + * 单机服务配置 + */ + private SingleServerConfig singleServerConfig; + + @Data + @NoArgsConstructor + public static class SingleServerConfig { + + /** + * 客户端名称 + */ + private String clientName; + + /** + * 最小空闲连接数 + */ + private int connectionMinimumIdleSize; + + /** + * 连接池大小 + */ + private int connectionPoolSize; + + /** + * 连接空闲超时,单位:毫秒 + */ + private int idleConnectionTimeout; + + /** + * 命令等待超时,单位:毫秒 + */ + private int timeout; + + /** + * 如果尝试在此限制之内发送成功,则开始启用 timeout 计时。 + */ + private int retryAttempts; + + /** + * 命令重试发送时间间隔,单位:毫秒 + */ + private int retryInterval; + + /** + * 发布和订阅连接的最小空闲连接数 + */ + private int subscriptionConnectionMinimumIdleSize; + + /** + * 发布和订阅连接池大小 + */ + private int subscriptionConnectionPoolSize; + + /** + * 单个连接最大订阅数量 + */ + private int subscriptionsPerConnection; + + /** + * DNS监测时间间隔,单位:毫秒 + */ + private int dnsMonitoringInterval; + + } + +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/properties/SwaggerProperties.java b/bashi-framework/src/main/java/com/bashi/framework/config/properties/SwaggerProperties.java new file mode 100644 index 0000000..32c3266 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/properties/SwaggerProperties.java @@ -0,0 +1,63 @@ +package com.bashi.framework.config.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * swagger 配置属性 + * + * @author Lion Li + */ +@Data +@Component +@ConfigurationProperties(prefix = "swagger") +public class SwaggerProperties { + + /** + * 验证码类型 + */ + private Boolean enabled; + /** + * 设置请求的统一前缀 + */ + private String pathMapping; + /** + * 验证码类别 + */ + private String title; + /** + * 数字验证码位数 + */ + private String description; + /** + * 字符验证码长度 + */ + private String version; + + /** + * 联系方式 + */ + private Contact contact; + + @Data + @NoArgsConstructor + public static class Contact{ + + /** + * 联系人 + **/ + private String name; + /** + * 联系人url + **/ + private String url; + /** + * 联系人email + **/ + private String email; + + } + +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/properties/ThreadPoolProperties.java b/bashi-framework/src/main/java/com/bashi/framework/config/properties/ThreadPoolProperties.java new file mode 100644 index 0000000..3a5e885 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/properties/ThreadPoolProperties.java @@ -0,0 +1,47 @@ +package com.bashi.framework.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 线程池 配置属性 + * + * @author Lion Li + */ +@Data +@Component +@ConfigurationProperties(prefix = "thread-pool") +public class ThreadPoolProperties { + + /** + * 是否开启线程池 + */ + private boolean enabled; + + /** + * 核心线程池大小 + */ + private int corePoolSize; + + /** + * 最大可创建的线程数 + */ + private int maxPoolSize; + + /** + * 队列最大长度 + */ + private int queueCapacity; + + /** + * 线程池维护线程所允许的空闲时间 + */ + private int keepAliveSeconds; + + /** + * 线程池对拒绝任务(无线程可用)的处理策略 + */ + private String rejectedExecutionHandler; + +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/properties/TokenProperties.java b/bashi-framework/src/main/java/com/bashi/framework/config/properties/TokenProperties.java new file mode 100644 index 0000000..ab22b42 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/properties/TokenProperties.java @@ -0,0 +1,31 @@ +package com.bashi.framework.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * token 配置属性 + * + * @author Lion Li + */ +@Data +@Component +@ConfigurationProperties(prefix = "token") +public class TokenProperties { + + /** + * 令牌自定义标识 + */ + private String header; + + /** + * 令牌秘钥 + */ + private String secret; + + /** + * 令牌有效期(默认30分钟) + */ + private int expireTime; +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/config/properties/XssProperties.java b/bashi-framework/src/main/java/com/bashi/framework/config/properties/XssProperties.java new file mode 100644 index 0000000..ab321ca --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/config/properties/XssProperties.java @@ -0,0 +1,32 @@ +package com.bashi.framework.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * xss过滤 配置属性 + * + * @author Lion Li + */ +@Data +@Component +@ConfigurationProperties(prefix = "xss") +public class XssProperties { + + /** + * 过滤开关 + */ + private String enabled; + + /** + * 排除链接(多个用逗号分隔) + */ + private String excludes; + + /** + * 匹配链接 + */ + private String urlPatterns; + +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/constant/CodeType.java b/bashi-framework/src/main/java/com/bashi/framework/constant/CodeType.java new file mode 100644 index 0000000..5db6ddd --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/constant/CodeType.java @@ -0,0 +1,17 @@ +package com.bashi.framework.constant; + +/** + *

created on 2021/7/15

+ * + * @author zhangliang + */ +public enum CodeType { + + LOGIN, + JOIN_MECHANIC, + JOIN_SHOP, + CUSTOMER_REGISTER, + CUSTOMER_FORGET_PASSWORD, + + +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/dto/WebSocketMessageDTO.java b/bashi-framework/src/main/java/com/bashi/framework/dto/WebSocketMessageDTO.java new file mode 100644 index 0000000..22babb0 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/dto/WebSocketMessageDTO.java @@ -0,0 +1,10 @@ +package com.bashi.framework.dto; + +import lombok.Data; + +@Data +public class WebSocketMessageDTO { + + private WebSocketMessageType type = WebSocketMessageType.ORDER_NOTIFY; + private String city; +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/dto/WebSocketMessageType.java b/bashi-framework/src/main/java/com/bashi/framework/dto/WebSocketMessageType.java new file mode 100644 index 0000000..7003624 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/dto/WebSocketMessageType.java @@ -0,0 +1,5 @@ +package com.bashi.framework.dto; + +public enum WebSocketMessageType { + ORDER_NOTIFY +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/interceptor/RepeatSubmitInterceptor.java b/bashi-framework/src/main/java/com/bashi/framework/interceptor/RepeatSubmitInterceptor.java new file mode 100644 index 0000000..8d9d797 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/interceptor/RepeatSubmitInterceptor.java @@ -0,0 +1,56 @@ +package com.bashi.framework.interceptor; + +import com.bashi.common.annotation.RepeatSubmit; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.utils.JsonUtils; +import com.bashi.common.utils.ServletUtils; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; + +/** + * 防止重复提交拦截器 + * + * @author duteliang + */ +@Component +public abstract class RepeatSubmitInterceptor extends HandlerInterceptorAdapter +{ + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception + { + if (handler instanceof HandlerMethod) + { + HandlerMethod handlerMethod = (HandlerMethod) handler; + Method method = handlerMethod.getMethod(); + RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class); + if (annotation != null) + { + if (this.isRepeatSubmit(request)) + { + AjaxResult ajaxResult = AjaxResult.error("不允许重复提交,请稍后再试"); + ServletUtils.renderString(response, JsonUtils.toJsonString(ajaxResult)); + return false; + } + } + return true; + } + else + { + return super.preHandle(request, response, handler); + } + } + + /** + * 验证是否重复提交由子类实现具体的防重复提交的规则 + * + * @param request + * @return + * @throws Exception + */ + public abstract boolean isRepeatSubmit(HttpServletRequest request); +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/interceptor/impl/SameUrlDataInterceptor.java b/bashi-framework/src/main/java/com/bashi/framework/interceptor/impl/SameUrlDataInterceptor.java new file mode 100644 index 0000000..f1c29f3 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/interceptor/impl/SameUrlDataInterceptor.java @@ -0,0 +1,133 @@ +package com.bashi.framework.interceptor.impl; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.lang.Validator; +import com.bashi.common.constant.Constants; +import com.bashi.common.core.redis.RedisCache; +import com.bashi.common.filter.RepeatedlyRequestWrapper; +import com.bashi.common.utils.JsonUtils; +import com.bashi.framework.interceptor.RepeatSubmitInterceptor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * 判断请求url和数据是否和上一次相同, + * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。 + * + * @author duteliang + */ +@Slf4j +@Component +public class SameUrlDataInterceptor extends RepeatSubmitInterceptor +{ + public final String REPEAT_PARAMS = "repeatParams"; + + public final String REPEAT_TIME = "repeatTime"; + + // 令牌自定义标识 + @Value("${token.header}") + private String header; + + @Autowired + private RedisCache redisCache; + + /** + * 间隔时间,单位:秒 默认10秒 + * + * 两次相同参数的请求,如果间隔时间大于该参数,系统不会认定为重复提交的数据 + */ + private int intervalTime = 10; + + public void setIntervalTime(int intervalTime) + { + this.intervalTime = intervalTime; + } + + @SuppressWarnings("unchecked") + @Override + public boolean isRepeatSubmit(HttpServletRequest request) + { + String nowParams = ""; + if (request instanceof RepeatedlyRequestWrapper) + { + RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request; + try { + nowParams = IoUtil.readUtf8(repeatedlyRequest.getInputStream()); + } catch (IOException e) { + log.warn("读取流出现问题!"); + } + } + + // body参数为空,获取Parameter的数据 + if (Validator.isEmpty(nowParams)) + { + nowParams = JsonUtils.toJsonString(request.getParameterMap()); + } + Map nowDataMap = new HashMap(); + nowDataMap.put(REPEAT_PARAMS, nowParams); + nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); + + // 请求地址(作为存放cache的key值) + String url = request.getRequestURI(); + + // 唯一值(没有消息头则使用请求地址) + String submitKey = request.getHeader(header); + if (Validator.isEmpty(submitKey)) + { + submitKey = url; + } + + // 唯一标识(指定key + 消息头) + String cacheRepeatKey = Constants.REPEAT_SUBMIT_KEY + submitKey; + + Object sessionObj = redisCache.getCacheObject(cacheRepeatKey); + if (sessionObj != null) + { + Map sessionMap = (Map) sessionObj; + if (sessionMap.containsKey(url)) + { + Map preDataMap = (Map) sessionMap.get(url); + if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap)) + { + return true; + } + } + } + Map cacheMap = new HashMap(); + cacheMap.put(url, nowDataMap); + redisCache.setCacheObject(cacheRepeatKey, cacheMap, intervalTime, TimeUnit.SECONDS); + return false; + } + + /** + * 判断参数是否相同 + */ + private boolean compareParams(Map nowMap, Map preMap) + { + String nowParams = (String) nowMap.get(REPEAT_PARAMS); + String preParams = (String) preMap.get(REPEAT_PARAMS); + return nowParams.equals(preParams); + } + + /** + * 判断两次间隔时间 + */ + private boolean compareTime(Map nowMap, Map preMap) + { + long time1 = (Long) nowMap.get(REPEAT_TIME); + long time2 = (Long) preMap.get(REPEAT_TIME); + if ((time1 - time2) < (this.intervalTime * 1000)) + { + return true; + } + return false; + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/manager/ShutdownManager.java b/bashi-framework/src/main/java/com/bashi/framework/manager/ShutdownManager.java new file mode 100644 index 0000000..effc78d --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/manager/ShutdownManager.java @@ -0,0 +1,41 @@ +package com.bashi.framework.manager; + +import com.bashi.common.utils.Threads; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; + +import javax.annotation.PreDestroy; +import java.util.concurrent.ScheduledExecutorService; + +/** + * 确保应用退出时能关闭后台线程 + * + * @author Lion Li + */ +@Slf4j(topic = "sys-user") +@Component +public class ShutdownManager { + + @Autowired + @Qualifier("scheduledExecutorService") + private ScheduledExecutorService scheduledExecutorService; + + @PreDestroy + public void destroy() { + shutdownAsyncManager(); + } + + /** + * 停止异步执行任务 + */ + private void shutdownAsyncManager() { + try { + log.info("====关闭后台任务任务线程池===="); + Threads.shutdownAndAwaitTermination(scheduledExecutorService); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/mybatisplus/CreateAndUpdateMetaObjectHandler.java b/bashi-framework/src/main/java/com/bashi/framework/mybatisplus/CreateAndUpdateMetaObjectHandler.java new file mode 100644 index 0000000..b67f3e1 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/mybatisplus/CreateAndUpdateMetaObjectHandler.java @@ -0,0 +1,45 @@ +package com.bashi.framework.mybatisplus; + +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.bashi.common.utils.SecurityUtils; +import org.apache.ibatis.reflection.MetaObject; + +import java.util.Date; + +/** + * MP注入处理器 + * @author Lion Li + * @date 2021/4/25 + */ +public class CreateAndUpdateMetaObjectHandler implements MetaObjectHandler { + + @Override + public void insertFill(MetaObject metaObject) { + //根据属性名字设置要填充的值 + if (metaObject.hasGetter("createTime")) { + if (metaObject.getValue("createTime") == null) { + this.setFieldValByName("createTime", new Date(), metaObject); + } + } + if (metaObject.hasGetter("createBy")) { + if (metaObject.getValue("createBy") == null) { + this.setFieldValByName("createBy", SecurityUtils.getUsername(), metaObject); + } + } + } + + @Override + public void updateFill(MetaObject metaObject) { + if (metaObject.hasGetter("updateBy")) { + if (metaObject.getValue("updateBy") == null) { + this.setFieldValByName("updateBy", SecurityUtils.getUsername(), metaObject); + } + } + if (metaObject.hasGetter("updateTime")) { + if (metaObject.getValue("updateTime") == null) { + this.setFieldValByName("updateTime", new Date(), metaObject); + } + } + } + +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/security/filter/JwtAuthenticationTokenFilter.java b/bashi-framework/src/main/java/com/bashi/framework/security/filter/JwtAuthenticationTokenFilter.java new file mode 100644 index 0000000..ec050df --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/security/filter/JwtAuthenticationTokenFilter.java @@ -0,0 +1,52 @@ +package com.bashi.framework.security.filter; + +import cn.hutool.core.lang.Validator; +import com.bashi.common.core.domain.model.LoginUser; +import com.bashi.common.utils.SecurityUtils; +import com.bashi.framework.web.service.TokenService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * token过滤器 验证token有效性 + * + * @author duteliang + */ +@Component +public class JwtAuthenticationTokenFilter extends OncePerRequestFilter +{ + @Autowired + private TokenService tokenService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException + { + LoginUser loginUser = tokenService.getLoginUser(request); + if (Validator.isNotNull(loginUser) && Validator.isNull(SecurityUtils.getAuthentication())) { + tokenService.verifyToken(loginUser); + if(loginUser.getUser() != null){ + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + }else if(loginUser.getCustomer() != null){ + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + } + + + } + chain.doFilter(request, response); + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/security/handle/AuthenticationEntryPointImpl.java b/bashi-framework/src/main/java/com/bashi/framework/security/handle/AuthenticationEntryPointImpl.java new file mode 100644 index 0000000..6b42c2c --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/security/handle/AuthenticationEntryPointImpl.java @@ -0,0 +1,35 @@ +package com.bashi.framework.security.handle; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpStatus; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.utils.JsonUtils; +import com.bashi.common.utils.ServletUtils; +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; +import java.io.Serializable; + +/** + * 认证失败处理类 返回未授权 + * + * @author duteliang + */ +@Component +public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable +{ + private static final long serialVersionUID = -8970718410437077606L; + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) + throws IOException + { + int code = HttpStatus.HTTP_UNAUTHORIZED; + String msg = StrUtil.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI()); + ServletUtils.renderString(response, JsonUtils.toJsonString(AjaxResult.error(code, msg))); + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/security/handle/LogoutSuccessHandlerImpl.java b/bashi-framework/src/main/java/com/bashi/framework/security/handle/LogoutSuccessHandlerImpl.java new file mode 100644 index 0000000..3b314fb --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/security/handle/LogoutSuccessHandlerImpl.java @@ -0,0 +1,53 @@ +package com.bashi.framework.security.handle; + +import cn.hutool.core.lang.Validator; +import cn.hutool.http.HttpStatus; +import com.bashi.common.constant.Constants; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.domain.model.LoginUser; +import com.bashi.common.utils.JsonUtils; +import com.bashi.common.utils.ServletUtils; +import com.bashi.framework.web.service.AsyncService; +import com.bashi.framework.web.service.TokenService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 自定义退出处理类 返回成功 + * + * @author duteliang + */ +@Configuration +public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler { + + @Autowired + private TokenService tokenService; + + @Autowired + private AsyncService asyncService; + + /** + * 退出处理 + */ + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws IOException, ServletException { + LoginUser loginUser = tokenService.getLoginUser(request); + if (Validator.isNotNull(loginUser)) { + String userName = loginUser.getUsername(); + // 删除用户缓存记录 + tokenService.delLoginUser(loginUser.getToken()); + // 记录用户退出日志 + asyncService.recordLogininfor(userName, Constants.LOGOUT, "退出成功", request); + } + ServletUtils.renderString(response, JsonUtils.toJsonString(AjaxResult.error(HttpStatus.HTTP_OK, "退出成功"))); + } + +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/security/sms/IDuteUserDetailsService.java b/bashi-framework/src/main/java/com/bashi/framework/security/sms/IDuteUserDetailsService.java new file mode 100644 index 0000000..369a8c2 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/security/sms/IDuteUserDetailsService.java @@ -0,0 +1,17 @@ +package com.bashi.framework.security.sms; + +import com.bashi.common.core.domain.model.LoginPhoneBody; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +/** + *

created on 2021/7/15

+ * + * @author zhangliang + */ +public interface IDuteUserDetailsService extends UserDetailsService { + + UserDetails loadUserByMobile(LoginPhoneBody username) throws UsernameNotFoundException; + +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/security/sms/LoginTypeEnums.java b/bashi-framework/src/main/java/com/bashi/framework/security/sms/LoginTypeEnums.java new file mode 100644 index 0000000..4a5d05c --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/security/sms/LoginTypeEnums.java @@ -0,0 +1,37 @@ +package com.bashi.framework.security.sms; + +public enum LoginTypeEnums { + USER(1,"用户"), + MECHANIC(2,"服务人员"), + SHOP(3,"商家"), + MP_OPEN_ID(4,"公众号openId登录"), + MP_PHONE(5,"公众号手机登录"), + CUSTOMER_PASSWORD(6,"客户密码登陆"), + ; + + private final Integer code; + private final String msg; + + LoginTypeEnums(Integer code, String msg) { + this.code = code; + this.msg = msg; + } + + public Integer getCode() { + return code; + } + + public String getMsg() { + return msg; + } + + public static String getMsgByCode(Integer code){ + LoginTypeEnums[] values = LoginTypeEnums.values(); + for (LoginTypeEnums value : values) { + if(value.getCode().equals(code)){ + return value.getMsg(); + } + } + return "用户"; + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/security/sms/SmsAuthenticationFailureHandler.java b/bashi-framework/src/main/java/com/bashi/framework/security/sms/SmsAuthenticationFailureHandler.java new file mode 100644 index 0000000..a31fbd9 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/security/sms/SmsAuthenticationFailureHandler.java @@ -0,0 +1,41 @@ +package com.bashi.framework.security.sms; + +import com.bashi.common.constant.Constants; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.utils.JsonUtils; +import com.bashi.common.utils.MessageUtils; +import com.bashi.framework.web.service.AsyncService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + *

created on 2021/7/14

+ * + * @author zhangliang + */ +@Component +@Slf4j +public class SmsAuthenticationFailureHandler implements AuthenticationFailureHandler { + @Autowired + private AsyncService asyncService; + public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile"; + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { + AjaxResult error = AjaxResult.error(exception.getMessage(), null); + String mobile = request.getParameter(SPRING_SECURITY_FORM_MOBILE_KEY); + asyncService.recordLogininfor(mobile, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"), request); + response.setStatus(HttpStatus.OK.value()); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(JsonUtils.toJsonString(error)); + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/security/sms/SmsAuthenticationFilter.java b/bashi-framework/src/main/java/com/bashi/framework/security/sms/SmsAuthenticationFilter.java new file mode 100644 index 0000000..2c5a7e1 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/security/sms/SmsAuthenticationFilter.java @@ -0,0 +1,94 @@ +package com.bashi.framework.security.sms; + +import com.bashi.framework.web.service.CodeService; +import org.springframework.lang.Nullable; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.util.Assert; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + *

created on 2021/7/13

+ * + * @author zhangliang + */ +public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter { + + public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile"; + private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY; + private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms/login", "POST"); + private boolean postOnly = true; + + private CodeService codeService; + + public CodeService getCodeService() { + return codeService; + } + + public void setCodeService(CodeService codeService) { + this.codeService = codeService; + } + + public SmsAuthenticationFilter() { + super(DEFAULT_ANT_PATH_REQUEST_MATCHER); + } + + public SmsAuthenticationFilter(AuthenticationManager authenticationManager) { + super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager); + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { + if (this.postOnly && !"POST".equals(request.getMethod())) { + throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); + } else { + String mobile = this.obtainMobile(request); + mobile = mobile != null ? mobile : ""; + mobile = mobile.trim(); + /*String mobileCode = this.obtainMobileCode(request); + mobileCode = mobileCode != null ? mobileCode : ""; + mobileCode = mobileCode.trim(); + if(!codeService.check(mobile, CodeType.LOGIN,mobileCode)){ + throw new AuthenticationServiceException("验证码错误"); + }*/ + SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile); + this.setDetails(request, authRequest); + return this.getAuthenticationManager().authenticate(authRequest); + } + } + + @Nullable + protected String obtainMobile(HttpServletRequest request) { + return request.getParameter(this.mobileParameter); + } + + @Nullable + protected String obtainMobileCode(HttpServletRequest request) { + return request.getParameter("mobileCode"); + } + + protected void setDetails(HttpServletRequest request, SmsAuthenticationToken authRequest) { + authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); + } + + public void setMobileParameter(String mobileParameter) { + Assert.hasText(mobileParameter, "Mobile parameter must not be empty or null"); + this.mobileParameter = mobileParameter; + } + + public String getMobileParameter() { + return mobileParameter; + } + + + public void setPostOnly(boolean postOnly) { + this.postOnly = postOnly; + } + +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/security/sms/SmsAuthenticationProvider.java b/bashi-framework/src/main/java/com/bashi/framework/security/sms/SmsAuthenticationProvider.java new file mode 100644 index 0000000..16f2662 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/security/sms/SmsAuthenticationProvider.java @@ -0,0 +1,60 @@ +package com.bashi.framework.security.sms; + +import com.bashi.common.core.domain.model.LoginPhoneBody; +import com.bashi.common.utils.JsonUtils; +import com.bashi.framework.web.service.CodeService; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetails; + +/** + *

created on 2021/7/13

+ * + * @author zhangliang + */ +public class SmsAuthenticationProvider implements AuthenticationProvider { + + private IDuteUserDetailsService userDetailsService; + + private CodeService codeService; + + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication; + + LoginPhoneBody mobile = (LoginPhoneBody) authenticationToken.getPrincipal(); + + UserDetails userDetails = userDetailsService.loadUserByMobile(mobile); + + // 此时鉴权成功后,应当重新 new 一个拥有鉴权的 authenticationResult 返回 + SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities()); + + authenticationResult.setDetails(authenticationToken.getDetails()); + + return authenticationResult; + } + + @Override + public boolean supports(Class authentication) { + // 判断 authentication 是不是 SmsCodeAuthenticationToken 的子类或子接口 + return SmsAuthenticationToken.class.isAssignableFrom(authentication); + } + + public IDuteUserDetailsService getUserDetailsService() { + return userDetailsService; + } + + public void setUserDetailsService(IDuteUserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + public CodeService getCodeService() { + return codeService; + } + + public void setCodeService(CodeService codeService) { + this.codeService = codeService; + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/security/sms/SmsAuthenticationSuccessHandler.java b/bashi-framework/src/main/java/com/bashi/framework/security/sms/SmsAuthenticationSuccessHandler.java new file mode 100644 index 0000000..aa63610 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/security/sms/SmsAuthenticationSuccessHandler.java @@ -0,0 +1,69 @@ +package com.bashi.framework.security.sms; + +import com.bashi.common.constant.Constants; +import com.bashi.common.core.domain.entity.SysUser; +import com.bashi.common.core.domain.model.LoginUser; +import com.bashi.common.utils.DateUtils; +import com.bashi.common.utils.MessageUtils; +import com.bashi.common.utils.ServletUtils; +import com.bashi.framework.web.service.AsyncService; +import com.bashi.framework.web.service.TokenService; +import com.bashi.system.service.ISysUserService; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + *

created on 2021/7/14

+ * + * @author zhangliang + */ +@Component +@Slf4j +public class SmsAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + + @Autowired + private ObjectMapper objectMapper; + @Autowired + private AsyncService asyncService; + @Autowired + private ISysUserService userService; + @Autowired + private TokenService tokenService; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws IOException { + LoginUser loginUser = (LoginUser) authentication.getPrincipal(); + asyncService.recordLogininfor(loginUser.getUsername(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"), request); + recordLoginInfo(loginUser.getUser()); + // 生成token + String token = tokenService.createToken(loginUser); + Map ajax = new HashMap<>(); + ajax.put("token",token); + ajax.put("code",200); +// ajax.put("data",loginUser); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(objectMapper.writeValueAsString(ajax)); + } + + /** + * 记录登录信息 + */ + public void recordLoginInfo(SysUser user) { + user.setLoginIp(ServletUtils.getClientIP()); + user.setLoginDate(DateUtils.getNowDate()); + user.setUpdateBy(user.getUserName()); + userService.updateUserProfile(user); + } + +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/security/sms/SmsAuthenticationToken.java b/bashi-framework/src/main/java/com/bashi/framework/security/sms/SmsAuthenticationToken.java new file mode 100644 index 0000000..58f90b7 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/security/sms/SmsAuthenticationToken.java @@ -0,0 +1,74 @@ +package com.bashi.framework.security.sms; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.util.Assert; + +import java.util.Collection; + +/** + *

created on 2021/7/13

+ * + * @author zhangliang + */ +public class SmsAuthenticationToken extends AbstractAuthenticationToken { + + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + + /** + * 手机号 + */ + private final Object principal; + + /** + * This constructor can be safely used by any code that wishes to create a + * UsernamePasswordAuthenticationToken, as the {@link #isAuthenticated()} + * will return false. + * + */ + public SmsAuthenticationToken(Object principal) { + super(null); + this.principal = principal; + setAuthenticated(false); + } + + /** + * This constructor should only be used by AuthenticationManager or + * AuthenticationProvider implementations that are satisfied with + * producing a trusted (i.e. {@link #isAuthenticated()} = true) + * authentication token. + * @param principal + * @param authorities + */ + public SmsAuthenticationToken(Object principal, + Collection authorities) { + super(authorities); + this.principal = principal; + super.setAuthenticated(true); // must use super, as we override + } + + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return this.principal; + } + + @Override + public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { + Assert.isTrue(!isAuthenticated, + "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); + super.setAuthenticated(false); + } + + @Override + public void eraseCredentials() { + super.eraseCredentials(); + } + +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/util/AgentUtils.java b/bashi-framework/src/main/java/com/bashi/framework/util/AgentUtils.java new file mode 100644 index 0000000..5cf74c9 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/util/AgentUtils.java @@ -0,0 +1,83 @@ +package com.bashi.framework.util; + +import cn.hutool.extra.servlet.ServletUtil; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import org.apache.http.HttpHeaders; + +import javax.servlet.http.HttpServletRequest; + +public class AgentUtils { + + public enum MarketEnum { + none("none","未知"), + iPhone("iPhone","苹果"), + MP("MP","公众号"), + huawei("huawei","华为"), + xiaomi("xiaomi","小米"), + a360("a360","360"), + vivo("vivo","vivo"), + baidu("baidu591","百度"), + share("share","分享"), + ; + + private String code; + private String name; + + MarketEnum(String code, String name) { + this.code = code; + this.name = name; + } + + public String getCode() { + return code; + } + + public String getName() { + return name; + } + + public static String getMarketValue(String key){ + MarketEnum[] values = MarketEnum.values(); + for (MarketEnum value : values) { + if(value.getCode().equals(key)){ + return value.getName(); + } + } + return key; + } + } + + + private final static String APP_MARKET = "channelId"; + + private final static String VERSION_NAME = "versionName"; + + public static String getVersion(HttpServletRequest request){ + return request.getHeader(VERSION_NAME); + } + + public static String getAgentCode(HttpServletRequest request){ + return request.getHeader(APP_MARKET); + } + + public static boolean checkIos(HttpServletRequest request){ + String agent = request.getHeader(HttpHeaders.USER_AGENT); + if(agent.contains("iPhone")|| agent.contains("iPod") || agent.contains("iPad")){ + return true; + } + return false; + } + + public static String getAgent(HttpServletRequest request){ + String market = request.getHeader(APP_MARKET); + if(StringUtils.isBlank(market)){ // 可能是IOS,也可能谁都不是 + String agent = request.getHeader(HttpHeaders.USER_AGENT); + if(agent.contains("iPhone")|| agent.contains("iPod") || agent.contains("iPad")){ + return MarketEnum.iPhone.getName(); + }else{ + return MarketEnum.none.getName(); + } + } + return MarketEnum.getMarketValue(market); + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/web/exception/GlobalExceptionHandler.java b/bashi-framework/src/main/java/com/bashi/framework/web/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..caf8537 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/web/exception/GlobalExceptionHandler.java @@ -0,0 +1,117 @@ +package com.bashi.framework.web.exception; + +import cn.hutool.core.lang.Validator; +import cn.hutool.http.HttpStatus; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.exception.BaseException; +import com.bashi.common.exception.CustomException; +import com.bashi.common.exception.DemoModeException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.AccountExpiredException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.validation.BindException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.NoHandlerFoundException; + +import javax.validation.ConstraintViolationException; + +/** + * 全局异常处理器 + * + * @author duteliang + */ +@RestControllerAdvice +public class GlobalExceptionHandler { + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + /** + * 基础异常 + */ + @ExceptionHandler(BaseException.class) + public AjaxResult baseException(BaseException e) { + return AjaxResult.error(e.getMessage()); + } + + /** + * 业务异常 + */ + @ExceptionHandler(CustomException.class) + public AjaxResult businessException(CustomException e) { + if (Validator.isNull(e.getCode())) { + return AjaxResult.error(e.getMessage()); + } + return AjaxResult.error(e.getCode(), e.getMessage()); + } + + @ExceptionHandler(NoHandlerFoundException.class) + public AjaxResult handlerNoFoundException(Exception e) { + log.error(e.getMessage(), e); + return AjaxResult.error(HttpStatus.HTTP_NOT_FOUND, "路径不存在,请检查路径是否正确"); + } + + @ExceptionHandler(AccessDeniedException.class) + public AjaxResult handleAuthorizationException(AccessDeniedException e) { + log.error(e.getMessage()); + return AjaxResult.error(HttpStatus.HTTP_FORBIDDEN, "没有权限,请联系管理员授权"); + } + + @ExceptionHandler(AccountExpiredException.class) + public AjaxResult handleAccountExpiredException(AccountExpiredException e) { + log.error(e.getMessage(), e); + return AjaxResult.error(e.getMessage()); + } + + @ExceptionHandler(UsernameNotFoundException.class) + public AjaxResult handleUsernameNotFoundException(UsernameNotFoundException e) { + log.error(e.getMessage(), e); + return AjaxResult.error(e.getMessage()); + } + + @ExceptionHandler(Exception.class) + public AjaxResult handleException(Exception e) { + log.error(e.getMessage(), e); + return AjaxResult.error(e.getMessage()); + } + + /** + * 自定义验证异常 + */ + @ExceptionHandler(BindException.class) + public AjaxResult validatedBindException(BindException e) { + log.error(e.getMessage(), e); + String message = e.getAllErrors().get(0).getDefaultMessage(); + return AjaxResult.error(message); + } + + /** + * 自定义验证异常 + */ + @ExceptionHandler(ConstraintViolationException.class) + public AjaxResult constraintViolationException(ConstraintViolationException e) { + log.error(e.getMessage(), e); + String message = e.getConstraintViolations().iterator().next().getMessage(); + return AjaxResult.error(message); + } + + /** + * 自定义验证异常 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public Object validExceptionHandler(MethodArgumentNotValidException e) { + log.error(e.getMessage(), e); + String message = e.getBindingResult().getFieldError().getDefaultMessage(); + return AjaxResult.error(message); + } + + /** + * 演示模式异常 + */ + @ExceptionHandler(DemoModeException.class) + public AjaxResult demoModeException(DemoModeException e) { + return AjaxResult.error("演示模式,不允许操作"); + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/web/service/AsyncService.java b/bashi-framework/src/main/java/com/bashi/framework/web/service/AsyncService.java new file mode 100644 index 0000000..7b8c8d1 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/web/service/AsyncService.java @@ -0,0 +1,100 @@ +package com.bashi.framework.web.service; + +import cn.hutool.http.useragent.UserAgent; +import cn.hutool.http.useragent.UserAgentUtil; +import com.bashi.common.constant.Constants; +import com.bashi.common.utils.ServletUtils; +import com.bashi.common.utils.ip.AddressUtils; +import com.bashi.system.domain.SysLogininfor; +import com.bashi.system.domain.SysOperLog; +import com.bashi.system.service.ISysLogininforService; +import com.bashi.system.service.ISysOperLogService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; + +/** + * 异步工厂(产生任务用) + * + * @author Lion Li + */ +@Slf4j(topic = "sys-user") +@Async +@Component +public class AsyncService { + + @Autowired + private ISysLogininforService iSysLogininforService; + + @Autowired + private ISysOperLogService iSysOperLogService; + + /** + * 记录登录信息 + * + * @param username 用户名 + * @param status 状态 + * @param message 消息 + * @param args 列表 + */ + public void recordLogininfor(final String username, final String status, final String message, + HttpServletRequest request, final Object... args) { + final UserAgent userAgent = UserAgentUtil.parse(request.getHeader("User-Agent")); + final String ip = ServletUtils.getClientIP(request); + + String address = AddressUtils.getRealAddressByIP(ip); + StringBuilder s = new StringBuilder(); + s.append(getBlock(ip)); + s.append(address); + s.append(getBlock(username)); + s.append(getBlock(status)); + s.append(getBlock(message)); + // 打印信息到日志 + log.info(s.toString(), args); + // 获取客户端操作系统 + String os = userAgent.getOs().getName(); + // 获取客户端浏览器 + String browser = userAgent.getBrowser().getName(); + // 封装对象 + SysLogininfor logininfor = new SysLogininfor(); + logininfor.setUserName(username); + logininfor.setIpaddr(ip); + logininfor.setLoginLocation(address); + logininfor.setBrowser(browser); + logininfor.setOs(os); + logininfor.setMsg(message); + // 日志状态 + if (Constants.LOGIN_SUCCESS.equals(status) || Constants.LOGOUT.equals(status)) { + logininfor.setStatus(Constants.SUCCESS); + } else if (Constants.LOGIN_FAIL.equals(status)) { + logininfor.setStatus(Constants.FAIL); + } + // 插入数据 + iSysLogininforService.insertLogininfor(logininfor); + } + + /** + * 操作日志记录 + * + * @param operLog 操作日志信息 + */ + public void recordOper(final SysOperLog operLog) { + // 远程查询操作地点 + operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp())); + iSysOperLogService.insertOperlog(operLog); + } + + private String getBlock(Object msg) { + if (msg == null) { + msg = ""; + } + return "[" + msg.toString() + "]"; + } + + public void recordLog(final StringBuilder logBuild) { + log.info(logBuild.toString()); + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/web/service/CodeService.java b/bashi-framework/src/main/java/com/bashi/framework/web/service/CodeService.java new file mode 100644 index 0000000..965ce72 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/web/service/CodeService.java @@ -0,0 +1,56 @@ +package com.bashi.framework.web.service; + +import com.bashi.framework.constant.CodeType; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.time.Duration; + +/** + *

created on 2021/7/15

+ * + * @author zhangliang + */ +@Component +public class CodeService { + + @Autowired + private StringRedisTemplate redisTemplate; + + private static final String AUTH_CODE = "AUTH:CODE"; + + private static final String CODE = "DUTE:CODE:"; + + private static final String TEST_ACCOUNT = "15312531253"; + + public void put(String mobile,CodeType type,String code){ + redisTemplate.opsForValue().set(CODE + type.name() + ":" + mobile,code, Duration.ofMinutes(11)); + } + + public boolean check(String mobile, CodeType type, String code){ + if(TEST_ACCOUNT.equals(mobile)){ + return true; + } + if(StringUtils.isBlank(code)){ + return false; + } + String authCode = redisTemplate.opsForValue().get(AUTH_CODE); + if(StringUtils.isNotEmpty(authCode)){ + String[] allCodeArray = authCode.split(","); + for (String allCode : allCodeArray) { + if(allCode.equals(code)){ + return true; + } + } + } + String s = redisTemplate.opsForValue().get(CODE + type.name() + ":" + mobile); + return code.equals(s); + } + + public void putAuthCode(String code){ + redisTemplate.opsForValue().set(AUTH_CODE,code); + } + +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/web/service/CustomerService.java b/bashi-framework/src/main/java/com/bashi/framework/web/service/CustomerService.java new file mode 100644 index 0000000..9924c7c --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/web/service/CustomerService.java @@ -0,0 +1,10 @@ +package com.bashi.framework.web.service; + +/** + *

created on 2021/7/30

+ * + * @author dute7liang + */ +public interface CustomerService { + +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/web/service/PermissionService.java b/bashi-framework/src/main/java/com/bashi/framework/web/service/PermissionService.java new file mode 100644 index 0000000..369496c --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/web/service/PermissionService.java @@ -0,0 +1,170 @@ +package com.bashi.framework.web.service; + +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.StrUtil; +import com.bashi.common.core.domain.entity.SysRole; +import com.bashi.common.core.domain.model.LoginUser; +import com.bashi.common.utils.ServletUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Set; + +/** + * 自定义权限实现,ss取自SpringSecurity首字母 + * + * @author duteliang + */ +@Service("ss") +public class PermissionService +{ + /** 所有权限标识 */ + private static final String ALL_PERMISSION = "*:*:*"; + + /** 管理员角色权限标识 */ + private static final String SUPER_ADMIN = "admin"; + + private static final String ROLE_DELIMETER = ","; + + private static final String PERMISSION_DELIMETER = ","; + + @Autowired + private TokenService tokenService; + + /** + * 验证用户是否具备某权限 + * + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + public boolean hasPermi(String permission) + { + if (Validator.isEmpty(permission)) + { + return false; + } + LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest()); + if (Validator.isNull(loginUser) || Validator.isEmpty(loginUser.getPermissions())) + { + return false; + } + return hasPermissions(loginUser.getPermissions(), permission); + } + + /** + * 验证用户是否不具备某权限,与 hasPermi逻辑相反 + * + * @param permission 权限字符串 + * @return 用户是否不具备某权限 + */ + public boolean lacksPermi(String permission) + { + return hasPermi(permission) != true; + } + + /** + * 验证用户是否具有以下任意一个权限 + * + * @param permissions 以 PERMISSION_NAMES_DELIMETER 为分隔符的权限列表 + * @return 用户是否具有以下任意一个权限 + */ + public boolean hasAnyPermi(String permissions) + { + if (Validator.isEmpty(permissions)) + { + return false; + } + LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest()); + if (Validator.isNull(loginUser) || Validator.isEmpty(loginUser.getPermissions())) + { + return false; + } + Set authorities = loginUser.getPermissions(); + for (String permission : permissions.split(PERMISSION_DELIMETER)) + { + if (permission != null && hasPermissions(authorities, permission)) + { + return true; + } + } + return false; + } + + /** + * 判断用户是否拥有某个角色 + * + * @param role 角色字符串 + * @return 用户是否具备某角色 + */ + public boolean hasRole(String role) + { + if (Validator.isEmpty(role)) + { + return false; + } + LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest()); + if (Validator.isNull(loginUser) || Validator.isEmpty(loginUser.getUser().getRoles())) + { + return false; + } + for (SysRole sysRole : loginUser.getUser().getRoles()) + { + String roleKey = sysRole.getRoleKey(); + if (SUPER_ADMIN.equals(roleKey) || roleKey.equals(StrUtil.trim(role))) + { + return true; + } + } + return false; + } + + /** + * 验证用户是否不具备某角色,与 isRole逻辑相反。 + * + * @param role 角色名称 + * @return 用户是否不具备某角色 + */ + public boolean lacksRole(String role) + { + return hasRole(role) != true; + } + + /** + * 验证用户是否具有以下任意一个角色 + * + * @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表 + * @return 用户是否具有以下任意一个角色 + */ + public boolean hasAnyRoles(String roles) + { + if (Validator.isEmpty(roles)) + { + return false; + } + LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest()); + if (Validator.isNull(loginUser) || Validator.isEmpty(loginUser.getUser().getRoles())) + { + return false; + } + for (String role : roles.split(ROLE_DELIMETER)) + { + if (hasRole(role)) + { + return true; + } + } + return false; + } + + /** + * 判断是否包含权限 + * + * @param permissions 权限列表 + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + private boolean hasPermissions(Set permissions, String permission) + { + return permissions.contains(ALL_PERMISSION) || permissions.contains(StrUtil.trim(permission)); + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/web/service/RegisterEventService.java b/bashi-framework/src/main/java/com/bashi/framework/web/service/RegisterEventService.java new file mode 100644 index 0000000..9cc59aa --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/web/service/RegisterEventService.java @@ -0,0 +1,13 @@ +package com.bashi.framework.web.service; + +import com.bashi.common.core.domain.entity.SysUser; + +/** + *

created on 2021/7/15

+ * + * @author zhangliang + */ +public interface RegisterEventService { + + void event(SysUser user); +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/web/service/SysLoginService.java b/bashi-framework/src/main/java/com/bashi/framework/web/service/SysLoginService.java new file mode 100644 index 0000000..a5c6242 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/web/service/SysLoginService.java @@ -0,0 +1,106 @@ +package com.bashi.framework.web.service; + +import com.bashi.common.constant.Constants; +import com.bashi.common.core.domain.entity.SysUser; +import com.bashi.common.core.domain.model.LoginUser; +import com.bashi.common.core.redis.RedisCache; +import com.bashi.common.exception.CustomException; +import com.bashi.common.exception.user.CaptchaException; +import com.bashi.common.exception.user.CaptchaExpireException; +import com.bashi.common.exception.user.UserPasswordNotMatchException; +import com.bashi.common.utils.DateUtils; +import com.bashi.common.utils.MessageUtils; +import com.bashi.common.utils.ServletUtils; +import com.bashi.framework.config.properties.CaptchaProperties; +import com.bashi.system.service.ISysUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; + +/** + * 登录校验方法 + * + * @author duteliang + */ +@Component +public class SysLoginService { + @Autowired + private TokenService tokenService; + + @Resource + private AuthenticationManager authenticationManager; + + @Autowired + private RedisCache redisCache; + + @Autowired + private CaptchaProperties captchaProperties; + + @Autowired + private ISysUserService userService; + + @Autowired + private AsyncService asyncService; + + /** + * 登录验证 + * + * @param username 用户名 + * @param password 密码 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public String login(String username, String password, String code, String uuid) { + HttpServletRequest request = ServletUtils.getRequest(); + if (captchaProperties.getEnabled()) { + String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid; + String captcha = redisCache.getCacheObject(verifyKey); + redisCache.deleteObject(verifyKey); + if (captcha == null) { + asyncService.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"), request); + throw new CaptchaExpireException(); + } + if (!code.equalsIgnoreCase(captcha)) { + asyncService.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"), request); + throw new CaptchaException(); + } + } + // 用户验证 + Authentication authentication = null; + try { + // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername + authentication = authenticationManager + .authenticate(new UsernamePasswordAuthenticationToken(username, password)); + } catch (Exception e) { + if (e instanceof BadCredentialsException) { + asyncService.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"), request); + throw new UserPasswordNotMatchException(); + } else { + asyncService.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage(), request); + throw new CustomException(e.getMessage()); + } + } + asyncService.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"), request); + LoginUser loginUser = (LoginUser) authentication.getPrincipal(); + recordLoginInfo(loginUser.getUser()); + // 生成token + return tokenService.createToken(loginUser); + } + + /** + * 记录登录信息 + */ + public void recordLoginInfo(SysUser user) { + user.setLoginIp(ServletUtils.getClientIP()); + user.setLoginDate(DateUtils.getNowDate()); + user.setUpdateBy(user.getUserName()); + userService.updateUserProfile(user); + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/web/service/SysPermissionService.java b/bashi-framework/src/main/java/com/bashi/framework/web/service/SysPermissionService.java new file mode 100644 index 0000000..1912f44 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/web/service/SysPermissionService.java @@ -0,0 +1,66 @@ +package com.bashi.framework.web.service; + +import java.util.HashSet; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import com.bashi.common.core.domain.entity.SysUser; +import com.bashi.system.service.ISysMenuService; +import com.bashi.system.service.ISysRoleService; + +/** + * 用户权限处理 + * + * @author duteliang + */ +@Component +public class SysPermissionService +{ + @Autowired + private ISysRoleService roleService; + + @Autowired + private ISysMenuService menuService; + + /** + * 获取角色数据权限 + * + * @param user 用户信息 + * @return 角色权限信息 + */ + public Set getRolePermission(SysUser user) + { + Set roles = new HashSet(); + // 管理员拥有所有权限 + if (user.isAdmin()) + { + roles.add("admin"); + } + else + { + roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId())); + } + return roles; + } + + /** + * 获取菜单数据权限 + * + * @param user 用户信息 + * @return 菜单权限信息 + */ + public Set getMenuPermission(SysUser user) + { + Set perms = new HashSet(); + // 管理员拥有所有权限 + if (user.isAdmin()) + { + perms.add("*:*:*"); + } + else + { + perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId())); + } + return perms; + } +} diff --git a/bashi-framework/src/main/java/com/bashi/framework/web/service/TokenService.java b/bashi-framework/src/main/java/com/bashi/framework/web/service/TokenService.java new file mode 100644 index 0000000..a786d68 --- /dev/null +++ b/bashi-framework/src/main/java/com/bashi/framework/web/service/TokenService.java @@ -0,0 +1,194 @@ +package com.bashi.framework.web.service; + +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.IdUtil; +import cn.hutool.http.useragent.UserAgent; +import cn.hutool.http.useragent.UserAgentUtil; +import com.bashi.common.constant.Constants; +import com.bashi.common.core.domain.model.LoginUser; +import com.bashi.common.core.redis.RedisCache; +import com.bashi.common.utils.ServletUtils; +import com.bashi.common.utils.ip.AddressUtils; +import com.bashi.framework.config.properties.TokenProperties; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * token验证处理 + * + * @author Lion Li + */ +@Component +public class TokenService { + + protected static final long MILLIS_SECOND = 1000; + + protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND; + + private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L; + + @Autowired + private RedisCache redisCache; + + @Autowired + private TokenProperties tokenProperties; + + /** + * 获取用户身份信息 + * + * @return 用户信息 + */ + public LoginUser getLoginUser(HttpServletRequest request) { + // 获取请求携带的令牌 + String token = getToken(request); + if (Validator.isNotEmpty(token)) { + Claims claims = parseToken(token); + // 解析对应的权限以及用户信息 + String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); + String userKey = getTokenKey(uuid); + LoginUser user = redisCache.getCacheObject(userKey); + return user; + } + return null; + } + + /** + * 设置用户身份信息 + */ + public void setLoginUser(LoginUser loginUser) { + if (Validator.isNotNull(loginUser) && Validator.isNotEmpty(loginUser.getToken())) { + refreshToken(loginUser); + } + } + + /** + * 删除用户身份信息 + */ + public void delLoginUser(String token) { + if (Validator.isNotEmpty(token)) { + String userKey = getTokenKey(token); + redisCache.deleteObject(userKey); + } + } + + /** + * 创建令牌 + * + * @param loginUser 用户信息 + * @return 令牌 + */ + public String createToken(LoginUser loginUser) { + String token = IdUtil.fastUUID(); + loginUser.setToken(token); + setUserAgent(loginUser); + refreshToken(loginUser); + + Map claims = new HashMap<>(); + claims.put(Constants.LOGIN_USER_KEY, token); + return createToken(claims); + } + + /** + * 验证令牌有效期,相差不足20分钟,自动刷新缓存 + * + * @param loginUser + * @return 令牌 + */ + public void verifyToken(LoginUser loginUser) { + long expireTime = loginUser.getExpireTime(); + long currentTime = System.currentTimeMillis(); + if (expireTime - currentTime <= MILLIS_MINUTE_TEN) { + refreshToken(loginUser); + } + } + + /** + * 刷新令牌有效期 + * + * @param loginUser 登录信息 + */ + public void refreshToken(LoginUser loginUser) { + loginUser.setLoginTime(System.currentTimeMillis()); + loginUser.setExpireTime(loginUser.getLoginTime() + tokenProperties.getExpireTime() * MILLIS_MINUTE); + // 根据uuid将loginUser缓存 + String userKey = getTokenKey(loginUser.getToken()); + redisCache.setCacheObject(userKey, loginUser, tokenProperties.getExpireTime(), TimeUnit.MINUTES); + } + + /** + * 设置用户代理信息 + * + * @param loginUser 登录信息 + */ + public void setUserAgent(LoginUser loginUser) { + UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent")); + String ip = ServletUtils.getClientIP(); + loginUser.setIpaddr(ip); + loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip)); + loginUser.setBrowser(userAgent.getBrowser().getName()); + loginUser.setOs(userAgent.getOs().getName()); + } + + /** + * 从数据声明生成令牌 + * + * @param claims 数据声明 + * @return 令牌 + */ + private String createToken(Map claims) { + String token = Jwts.builder() + .setClaims(claims) + .signWith(SignatureAlgorithm.HS512, tokenProperties.getSecret()).compact(); + return token; + } + + /** + * 从令牌中获取数据声明 + * + * @param token 令牌 + * @return 数据声明 + */ + private Claims parseToken(String token) { + return Jwts.parser() + .setSigningKey(tokenProperties.getSecret()) + .parseClaimsJws(token) + .getBody(); + } + + /** + * 从令牌中获取用户名 + * + * @param token 令牌 + * @return 用户名 + */ + public String getUsernameFromToken(String token) { + Claims claims = parseToken(token); + return claims.getSubject(); + } + + /** + * 获取请求token + * + * @param request + * @return token + */ + private String getToken(HttpServletRequest request) { + String token = request.getHeader(tokenProperties.getHeader()); + if (Validator.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) { + token = token.replace(Constants.TOKEN_PREFIX, ""); + } + return token; + } + + private String getTokenKey(String uuid) { + return Constants.LOGIN_TOKEN_KEY + uuid; + } +} diff --git a/bashi-generator/pom.xml b/bashi-generator/pom.xml new file mode 100644 index 0000000..b7a2f4a --- /dev/null +++ b/bashi-generator/pom.xml @@ -0,0 +1,34 @@ + + + + bashi + com.bashi + 2.4.0 + + 4.0.0 + + bashi-generator + + + generator代码生成 + + + + + + + org.apache.velocity + velocity + + + + + com.bashi + bashi-common + + + + + diff --git a/bashi-generator/src/main/java/com/bashi/generator/config/GenConfig.java b/bashi-generator/src/main/java/com/bashi/generator/config/GenConfig.java new file mode 100644 index 0000000..89fb057 --- /dev/null +++ b/bashi-generator/src/main/java/com/bashi/generator/config/GenConfig.java @@ -0,0 +1,73 @@ +package com.bashi.generator.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Component; + +/** + * 读取代码生成相关配置 + * + * @author duteliang + */ +@Component +@ConfigurationProperties(prefix = "gen") +@PropertySource(value = { "classpath:generator.yml" }) +public class GenConfig +{ + /** 作者 */ + public static String author; + + /** 生成包路径 */ + public static String packageName; + + /** 自动去除表前缀,默认是false */ + public static boolean autoRemovePre; + + /** 表前缀(类名不会包含表前缀) */ + public static String tablePrefix; + + public static String getAuthor() + { + return author; + } + + @Value("${author}") + public void setAuthor(String author) + { + GenConfig.author = author; + } + + public static String getPackageName() + { + return packageName; + } + + @Value("${packageName}") + public void setPackageName(String packageName) + { + GenConfig.packageName = packageName; + } + + public static boolean getAutoRemovePre() + { + return autoRemovePre; + } + + @Value("${autoRemovePre}") + public void setAutoRemovePre(boolean autoRemovePre) + { + GenConfig.autoRemovePre = autoRemovePre; + } + + public static String getTablePrefix() + { + return tablePrefix; + } + + @Value("${tablePrefix}") + public void setTablePrefix(String tablePrefix) + { + GenConfig.tablePrefix = tablePrefix; + } +} diff --git a/bashi-generator/src/main/java/com/bashi/generator/controller/GenController.java b/bashi-generator/src/main/java/com/bashi/generator/controller/GenController.java new file mode 100644 index 0000000..d0a10fc --- /dev/null +++ b/bashi-generator/src/main/java/com/bashi/generator/controller/GenController.java @@ -0,0 +1,204 @@ +package com.bashi.generator.controller; + +import cn.hutool.core.convert.Convert; +import com.bashi.common.annotation.Log; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.enums.BusinessType; +import com.bashi.generator.domain.GenTable; +import com.bashi.generator.domain.GenTableColumn; +import com.bashi.generator.service.IGenTableColumnService; +import com.bashi.generator.service.IGenTableService; +import org.apache.commons.io.IOUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 代码生成 操作处理 + * + * @author duteliang + */ +@RestController +@RequestMapping("/tool/gen") +public class GenController extends BaseController +{ + @Autowired + private IGenTableService genTableService; + + @Autowired + private IGenTableColumnService genTableColumnService; + + /** + * 查询代码生成列表 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:list')") + @GetMapping("/list") + public TableDataInfo genList(GenTable genTable) + { + return genTableService.selectPageGenTableList(genTable); + } + + /** + * 修改代码生成业务 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:query')") + @GetMapping(value = "/{talbleId}") + public AjaxResult getInfo(@PathVariable Long talbleId) + { + GenTable table = genTableService.selectGenTableById(talbleId); + List tables = genTableService.selectGenTableAll(); + List list = genTableColumnService.selectGenTableColumnListByTableId(talbleId); + Map map = new HashMap(); + map.put("info", table); + map.put("rows", list); + map.put("tables", tables); + return AjaxResult.success(map); + } + + /** + * 查询数据库列表 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:list')") + @GetMapping("/db/list") + public TableDataInfo dataList(GenTable genTable) + { + return genTableService.selectPageDbTableList(genTable); + } + + /** + * 查询数据表字段列表 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:list')") + @GetMapping(value = "/column/{talbleId}") + public TableDataInfo columnList(Long tableId) + { + TableDataInfo dataInfo = new TableDataInfo(); + List list = genTableColumnService.selectGenTableColumnListByTableId(tableId); + dataInfo.setRows(list); + dataInfo.setTotal(list.size()); + return dataInfo; + } + + /** + * 导入表结构(保存) + */ + @PreAuthorize("@ss.hasPermi('tool:gen:import')") + @Log(title = "代码生成", businessType = BusinessType.IMPORT) + @PostMapping("/importTable") + public AjaxResult importTableSave(String tables) + { + String[] tableNames = Convert.toStrArray(tables); + // 查询表信息 + List tableList = genTableService.selectDbTableListByNames(tableNames); + genTableService.importGenTable(tableList); + return AjaxResult.success(); + } + + /** + * 修改保存代码生成业务 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:edit')") + @Log(title = "代码生成", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult editSave(@Validated @RequestBody GenTable genTable) + { + genTableService.validateEdit(genTable); + genTableService.updateGenTable(genTable); + return AjaxResult.success(); + } + + /** + * 删除代码生成 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:remove')") + @Log(title = "代码生成", businessType = BusinessType.DELETE) + @DeleteMapping("/{tableIds}") + public AjaxResult remove(@PathVariable Long[] tableIds) + { + genTableService.deleteGenTableByIds(tableIds); + return AjaxResult.success(); + } + + /** + * 预览代码 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:preview')") + @GetMapping("/preview/{tableId}") + public AjaxResult preview(@PathVariable("tableId") Long tableId) throws IOException + { + Map dataMap = genTableService.previewCode(tableId); + return AjaxResult.success(dataMap); + } + + /** + * 生成代码(下载方式) + */ + @PreAuthorize("@ss.hasPermi('tool:gen:code')") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/download/{tableName}") + public void download(HttpServletResponse response, @PathVariable("tableName") String tableName) throws IOException + { + byte[] data = genTableService.downloadCode(tableName); + genCode(response, data); + } + + /** + * 生成代码(自定义路径) + */ + @PreAuthorize("@ss.hasPermi('tool:gen:code')") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/genCode/{tableName}") + public AjaxResult genCode(@PathVariable("tableName") String tableName) + { + genTableService.generatorCode(tableName); + return AjaxResult.success(); + } + + /** + * 同步数据库 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:edit')") + @Log(title = "代码生成", businessType = BusinessType.UPDATE) + @GetMapping("/synchDb/{tableName}") + public AjaxResult synchDb(@PathVariable("tableName") String tableName) + { + genTableService.synchDb(tableName); + return AjaxResult.success(); + } + + /** + * 批量生成代码 + */ + @PreAuthorize("@ss.hasPermi('tool:gen:code')") + @Log(title = "代码生成", businessType = BusinessType.GENCODE) + @GetMapping("/batchGenCode") + public void batchGenCode(HttpServletResponse response, String tables) throws IOException + { + String[] tableNames = Convert.toStrArray(tables); + byte[] data = genTableService.downloadCode(tableNames); + genCode(response, data); + } + + /** + * 生成zip文件 + */ + private void genCode(HttpServletResponse response, byte[] data) throws IOException + { + response.reset(); + response.addHeader("Access-Control-Allow-Origin", "*"); + response.addHeader("Access-Control-Expose-Headers", "Content-Disposition"); + response.setHeader("Content-Disposition", "attachment; filename=\"caocao.zip\""); + response.addHeader("Content-Length", "" + data.length); + response.setContentType("application/octet-stream; charset=UTF-8"); + IOUtils.write(data, response.getOutputStream()); + } +} diff --git a/bashi-generator/src/main/java/com/bashi/generator/domain/GenTable.java b/bashi-generator/src/main/java/com/bashi/generator/domain/GenTable.java new file mode 100644 index 0000000..3a2a7f4 --- /dev/null +++ b/bashi-generator/src/main/java/com/bashi/generator/domain/GenTable.java @@ -0,0 +1,237 @@ +package com.bashi.generator.domain; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.bashi.common.constant.GenConstants; +import lombok.*; +import lombok.experimental.Accessors; +import org.apache.commons.lang3.ArrayUtils; + +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 业务表 gen_table + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +@TableName("gen_table") +public class GenTable implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 编号 + */ + @TableId(value = "table_id", type = IdType.AUTO) + private Long tableId; + + /** + * 表名称 + */ + @NotBlank(message = "表名称不能为空") + private String tableName; + + /** + * 表描述 + */ + @NotBlank(message = "表描述不能为空") + private String tableComment; + + /** + * 关联父表的表名 + */ + private String subTableName; + + /** + * 本表关联父表的外键名 + */ + private String subTableFkName; + + /** + * 实体类名称(首字母大写) + */ + @NotBlank(message = "实体类名称不能为空") + private String className; + + /** + * 使用的模板(crud单表操作 tree树表操作 sub主子表操作) + */ + private String tplCategory; + + /** + * 生成包路径 + */ + @NotBlank(message = "生成包路径不能为空") + private String packageName; + + /** + * 生成模块名 + */ + @NotBlank(message = "生成模块名不能为空") + private String moduleName; + + /** + * 生成业务名 + */ + @NotBlank(message = "生成业务名不能为空") + private String businessName; + + /** + * 生成功能名 + */ + @NotBlank(message = "生成功能名不能为空") + private String functionName; + + /** + * 生成作者 + */ + @NotBlank(message = "作者不能为空") + private String functionAuthor; + + /** + * 生成代码方式(0zip压缩包 1自定义路径) + */ + private String genType; + + /** + * 生成路径(不填默认项目路径) + */ + private String genPath; + + /** + * 主键信息 + */ + @TableField(exist = false) + private GenTableColumn pkColumn; + + /** + * 子表信息 + */ + @TableField(exist = false) + private GenTable subTable; + + /** + * 表列信息 + */ + @Valid + @TableField(exist = false) + private List columns; + + /** + * 其它生成选项 + */ + private String options; + + /** + * 创建者 + */ + @TableField(fill = FieldFill.INSERT) + private String createBy; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 更新者 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateBy; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** + * 备注 + */ + private String remark; + + /** + * 请求参数 + */ + @TableField(exist = false) + private Map params = new HashMap<>(); + + /** + * 树编码字段 + */ + @TableField(exist = false) + private String treeCode; + + /** + * 树父编码字段 + */ + @TableField(exist = false) + private String treeParentCode; + + /** + * 树名称字段 + */ + @TableField(exist = false) + private String treeName; + + /** + * 上级菜单ID字段 + */ + @TableField(exist = false) + private String parentMenuId; + + /** + * 上级菜单名称字段 + */ + @TableField(exist = false) + private String parentMenuName; + + public boolean isSub() { + return isSub(this.tplCategory); + } + + public static boolean isSub(String tplCategory) { + return tplCategory != null && StrUtil.equals(GenConstants.TPL_SUB, tplCategory); + } + + public boolean isTree() { + return isTree(this.tplCategory); + } + + public static boolean isTree(String tplCategory) { + return tplCategory != null && StrUtil.equals(GenConstants.TPL_TREE, tplCategory); + } + + public boolean isCrud() { + return isCrud(this.tplCategory); + } + + public static boolean isCrud(String tplCategory) { + return tplCategory != null && StrUtil.equals(GenConstants.TPL_CRUD, tplCategory); + } + + public boolean isSuperColumn(String javaField) { + return isSuperColumn(this.tplCategory, javaField); + } + + public static boolean isSuperColumn(String tplCategory, String javaField) { + if (isTree(tplCategory)) { + return StrUtil.equalsAnyIgnoreCase(javaField, + ArrayUtils.addAll(GenConstants.TREE_ENTITY, GenConstants.BASE_ENTITY)); + } + return StrUtil.equalsAnyIgnoreCase(javaField, GenConstants.BASE_ENTITY); + } +} diff --git a/bashi-generator/src/main/java/com/bashi/generator/domain/GenTableColumn.java b/bashi-generator/src/main/java/com/bashi/generator/domain/GenTableColumn.java new file mode 100644 index 0000000..45a44ca --- /dev/null +++ b/bashi-generator/src/main/java/com/bashi/generator/domain/GenTableColumn.java @@ -0,0 +1,249 @@ +package com.bashi.generator.domain; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.*; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotBlank; +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * 代码生成业务字段表 gen_table_column + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +@TableName("gen_table_column") +public class GenTableColumn implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 编号 + */ + @TableId(value = "column_id", type = IdType.AUTO) + private Long columnId; + + /** + * 归属表编号 + */ + private Long tableId; + + /** + * 列名称 + */ + private String columnName; + + /** + * 列描述 + */ + private String columnComment; + + /** + * 列类型 + */ + private String columnType; + + /** + * JAVA类型 + */ + private String javaType; + + /** + * JAVA字段名 + */ + @NotBlank(message = "Java属性不能为空") + private String javaField; + + /** + * 是否主键(1是) + */ + private String isPk; + + /** + * 是否自增(1是) + */ + private String isIncrement; + + /** + * 是否必填(1是) + */ + private String isRequired; + + /** + * 是否为插入字段(1是) + */ + private String isInsert; + + /** + * 是否编辑字段(1是) + */ + private String isEdit; + + /** + * 是否列表字段(1是) + */ + private String isList; + + /** + * 是否查询字段(1是) + */ + private String isQuery; + + /** + * 查询方式(EQ等于、NE不等于、GT大于、LT小于、LIKE模糊、BETWEEN范围) + */ + private String queryType; + + /** + * 显示类型(input文本框、textarea文本域、select下拉框、checkbox复选框、radio单选框、datetime日期控件、image图片上传控件、upload文件上传控件、editor富文本控件) + */ + private String htmlType; + + /** + * 字典类型 + */ + private String dictType; + + /** + * 排序 + */ + private Integer sort; + + /** + * 创建者 + */ + @TableField(fill = FieldFill.INSERT) + private String createBy; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 更新者 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateBy; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** + * 请求参数 + */ + @TableField(exist = false) + private Map params = new HashMap<>(); + + public String getCapJavaField() { + return StrUtil.upperFirst(javaField); + } + + public boolean isPk() { + return isPk(this.isPk); + } + + public boolean isPk(String isPk) { + return isPk != null && StrUtil.equals("1", isPk); + } + + public boolean isIncrement() { + return isIncrement(this.isIncrement); + } + + public boolean isIncrement(String isIncrement) { + return isIncrement != null && StrUtil.equals("1", isIncrement); + } + + public boolean isRequired() { + return isRequired(this.isRequired); + } + + public boolean isRequired(String isRequired) { + return isRequired != null && StrUtil.equals("1", isRequired); + } + + public boolean isInsert() { + return isInsert(this.isInsert); + } + + public boolean isInsert(String isInsert) { + return isInsert != null && StrUtil.equals("1", isInsert); + } + + public boolean isEdit() { + return isInsert(this.isEdit); + } + + public boolean isEdit(String isEdit) { + return isEdit != null && StrUtil.equals("1", isEdit); + } + + public boolean isList() { + return isList(this.isList); + } + + public boolean isList(String isList) { + return isList != null && StrUtil.equals("1", isList); + } + + public boolean isQuery() { + return isQuery(this.isQuery); + } + + public boolean isQuery(String isQuery) { + return isQuery != null && StrUtil.equals("1", isQuery); + } + + public boolean isSuperColumn() { + return isSuperColumn(this.javaField); + } + + public static boolean isSuperColumn(String javaField) { + return StrUtil.equalsAnyIgnoreCase(javaField, + // BaseEntity + "createBy", "createTime", "updateBy", "updateTime", "remark", + // TreeEntity + "parentName", "parentId", "orderNum", "ancestors"); + } + + public boolean isUsableColumn() { + return isUsableColumn(javaField); + } + + public static boolean isUsableColumn(String javaField) { + // isSuperColumn()中的名单用于避免生成多余Domain属性,若某些属性在生成页面时需要用到不能忽略,则放在此处白名单 + return StrUtil.equalsAnyIgnoreCase(javaField, "parentId", "orderNum", "remark"); + } + + public String readConverterExp() { + String remarks = StrUtil.subBetween(this.columnComment, "(", ")"); + StringBuffer sb = new StringBuffer(); + if (StrUtil.isNotEmpty(remarks)) { + for (String value : remarks.split(" ")) { + if (StrUtil.isNotEmpty(value)) { + Object startStr = value.subSequence(0, 1); + String endStr = value.substring(1); + sb.append("").append(startStr).append("=").append(endStr).append(","); + } + } + return sb.deleteCharAt(sb.length() - 1).toString(); + } else { + return this.columnComment; + } + } +} diff --git a/bashi-generator/src/main/java/com/bashi/generator/mapper/GenTableColumnMapper.java b/bashi-generator/src/main/java/com/bashi/generator/mapper/GenTableColumnMapper.java new file mode 100644 index 0000000..d257f20 --- /dev/null +++ b/bashi-generator/src/main/java/com/bashi/generator/mapper/GenTableColumnMapper.java @@ -0,0 +1,22 @@ +package com.bashi.generator.mapper; + +import com.bashi.common.core.mybatisplus.core.BaseMapperPlus; +import com.bashi.generator.domain.GenTableColumn; + +import java.util.List; + +/** + * 业务字段 数据层 + * + * @author duteliang + */ +public interface GenTableColumnMapper extends BaseMapperPlus { + /** + * 根据表名称查询列信息 + * + * @param tableName 表名称 + * @return 列信息 + */ + public List selectDbTableColumnsByName(String tableName); + +} diff --git a/bashi-generator/src/main/java/com/bashi/generator/mapper/GenTableMapper.java b/bashi-generator/src/main/java/com/bashi/generator/mapper/GenTableMapper.java new file mode 100644 index 0000000..304dc02 --- /dev/null +++ b/bashi-generator/src/main/java/com/bashi/generator/mapper/GenTableMapper.java @@ -0,0 +1,69 @@ +package com.bashi.generator.mapper; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.bashi.common.core.mybatisplus.core.BaseMapperPlus; +import com.bashi.generator.domain.GenTable; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 业务 数据层 + * + * @author duteliang + */ +public interface GenTableMapper extends BaseMapperPlus { + + + Page selectPageGenTableList(@Param("page") Page page, @Param("genTable") GenTable genTable); + + Page selectPageDbTableList(@Param("page") Page page, @Param("genTable") GenTable genTable); + + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + public List selectGenTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + public List selectDbTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + public List selectDbTableListByNames(String[] tableNames); + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + public List selectGenTableAll(); + + /** + * 查询表ID业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + public GenTable selectGenTableById(Long id); + + /** + * 查询表名称业务信息 + * + * @param tableName 表名称 + * @return 业务信息 + */ + public GenTable selectGenTableByName(String tableName); + +} diff --git a/bashi-generator/src/main/java/com/bashi/generator/service/GenTableColumnServiceImpl.java b/bashi-generator/src/main/java/com/bashi/generator/service/GenTableColumnServiceImpl.java new file mode 100644 index 0000000..0350b15 --- /dev/null +++ b/bashi-generator/src/main/java/com/bashi/generator/service/GenTableColumnServiceImpl.java @@ -0,0 +1,65 @@ +package com.bashi.generator.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bashi.generator.domain.GenTableColumn; +import com.bashi.generator.mapper.GenTableColumnMapper; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.List; + +/** + * 业务字段 服务层实现 + * + * @author duteliang + */ +@Service +public class GenTableColumnServiceImpl extends ServiceImpl implements IGenTableColumnService { + + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + @Override + public List selectGenTableColumnListByTableId(Long tableId) { + return list(new LambdaQueryWrapper() + .eq(GenTableColumn::getTableId,tableId) + .orderByAsc(GenTableColumn::getSort)); + } + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + @Override + public int insertGenTableColumn(GenTableColumn genTableColumn) { + return baseMapper.insert(genTableColumn); + } + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + @Override + public int updateGenTableColumn(GenTableColumn genTableColumn) { + return baseMapper.updateById(genTableColumn); + } + + /** + * 删除业务字段对象 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + @Override + public int deleteGenTableColumnByIds(String ids) { + return baseMapper.deleteBatchIds(Arrays.asList(ids.split(","))); + } +} diff --git a/bashi-generator/src/main/java/com/bashi/generator/service/GenTableServiceImpl.java b/bashi-generator/src/main/java/com/bashi/generator/service/GenTableServiceImpl.java new file mode 100644 index 0000000..e3e65ee --- /dev/null +++ b/bashi-generator/src/main/java/com/bashi/generator/service/GenTableServiceImpl.java @@ -0,0 +1,457 @@ +package com.bashi.generator.service; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bashi.common.constant.Constants; +import com.bashi.common.constant.GenConstants; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.exception.CustomException; +import com.bashi.common.utils.JsonUtils; +import com.bashi.common.utils.PageUtils; +import com.bashi.common.utils.SecurityUtils; +import com.bashi.common.utils.file.FileUtils; +import com.bashi.generator.domain.GenTable; +import com.bashi.generator.domain.GenTableColumn; +import com.bashi.generator.mapper.GenTableColumnMapper; +import com.bashi.generator.mapper.GenTableMapper; +import com.bashi.generator.util.GenUtils; +import com.bashi.generator.util.VelocityInitializer; +import com.bashi.generator.util.VelocityUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; +import org.apache.velocity.Template; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.Velocity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * 业务 服务层实现 + * + * @author duteliang + */ +@Slf4j +@Service +public class GenTableServiceImpl extends ServiceImpl implements IGenTableService { + + @Autowired + private GenTableColumnMapper genTableColumnMapper; + + /** + * 查询业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + @Override + public GenTable selectGenTableById(Long id) { + GenTable genTable = baseMapper.selectGenTableById(id); + setTableFromOptions(genTable); + return genTable; + } + + @Override + public TableDataInfo selectPageGenTableList(GenTable genTable) { + return PageUtils.buildDataInfo(baseMapper.selectPageGenTableList(PageUtils.buildPage(), genTable)); + } + + @Override + public TableDataInfo selectPageDbTableList(GenTable genTable) { + return PageUtils.buildDataInfo(baseMapper.selectPageDbTableList(PageUtils.buildPage(), genTable)); + } + + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + @Override + public List selectGenTableList(GenTable genTable) { + return baseMapper.selectGenTableList(genTable); + } + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + @Override + public List selectDbTableList(GenTable genTable) { + return baseMapper.selectDbTableList(genTable); + } + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + @Override + public List selectDbTableListByNames(String[] tableNames) { + return baseMapper.selectDbTableListByNames(tableNames); + } + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + @Override + public List selectGenTableAll() { + return baseMapper.selectGenTableAll(); + } + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + @Override + @Transactional + public void updateGenTable(GenTable genTable) { + String options = JsonUtils.toJsonString(genTable.getParams()); + genTable.setOptions(options); + int row = baseMapper.updateById(genTable); + if (row > 0) { + for (GenTableColumn cenTableColumn : genTable.getColumns()) { + genTableColumnMapper.update(cenTableColumn, + new LambdaUpdateWrapper() + .set(cenTableColumn.getIsPk() == null, GenTableColumn::getIsPk, null) + .set(cenTableColumn.getIsIncrement() == null, GenTableColumn::getIsIncrement, null) + .set(cenTableColumn.getIsInsert() == null, GenTableColumn::getIsInsert, null) + .set(cenTableColumn.getIsEdit() == null, GenTableColumn::getIsEdit, null) + .set(cenTableColumn.getIsList() == null, GenTableColumn::getIsList, null) + .set(cenTableColumn.getIsQuery() == null, GenTableColumn::getIsQuery, null) + .set(cenTableColumn.getIsRequired() == null, GenTableColumn::getIsRequired, null) + .eq(GenTableColumn::getColumnId,cenTableColumn.getColumnId())); + } + } + } + + /** + * 删除业务对象 + * + * @param tableIds 需要删除的数据ID + * @return 结果 + */ + @Override + @Transactional + public void deleteGenTableByIds(Long[] tableIds) { + List ids = Arrays.asList(tableIds); + removeByIds(ids); + genTableColumnMapper.delete(new LambdaQueryWrapper().in(GenTableColumn::getTableId, ids)); + } + + /** + * 导入表结构 + * + * @param tableList 导入表列表 + */ + @Override + @Transactional + public void importGenTable(List tableList) { + String operName = SecurityUtils.getUsername(); + try { + for (GenTable table : tableList) { + String tableName = table.getTableName(); + GenUtils.initTable(table, operName); + int row = baseMapper.insert(table); + if (row > 0) { + // 保存列信息 + List genTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName); + for (GenTableColumn column : genTableColumns) { + GenUtils.initColumnField(column, table); + genTableColumnMapper.insert(column); + } + } + } + } catch (Exception e) { + throw new CustomException("导入失败:" + e.getMessage()); + } + } + + /** + * 预览代码 + * + * @param tableId 表编号 + * @return 预览数据列表 + */ + @Override + public Map previewCode(Long tableId) { + Map dataMap = new LinkedHashMap<>(); + // 查询表信息 + GenTable table = baseMapper.selectGenTableById(tableId); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table.getTplCategory()); + for (String template : templates) { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + dataMap.put(template, sw.toString()); + } + return dataMap; + } + + /** + * 生成代码(下载方式) + * + * @param tableName 表名称 + * @return 数据 + */ + @Override + public byte[] downloadCode(String tableName) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream); + generatorCode(tableName, zip); + IOUtils.closeQuietly(zip); + return outputStream.toByteArray(); + } + + /** + * 生成代码(自定义路径) + * + * @param tableName 表名称 + */ + @Override + public void generatorCode(String tableName) { + // 查询表信息 + GenTable table = baseMapper.selectGenTableByName(tableName); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table.getTplCategory()); + for (String template : templates) { + if (!StrUtil.containsAny(template, "sql.vm", "api.js.vm", "index.vue.vm", "index-tree.vue.vm")) { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + String path = getGenPath(table, template); + FileUtils.writeUtf8String(sw.toString(), path); + } + } + } + + /** + * 同步数据库 + * + * @param tableName 表名称 + */ + @Override + @Transactional + public void synchDb(String tableName) { + GenTable table = baseMapper.selectGenTableByName(tableName); + List tableColumns = table.getColumns(); + List tableColumnNames = tableColumns.stream().map(GenTableColumn::getColumnName).collect(Collectors.toList()); + + List dbTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName); + if (Validator.isEmpty(dbTableColumns)) { + throw new CustomException("同步数据失败,原表结构不存在"); + } + List dbTableColumnNames = dbTableColumns.stream().map(GenTableColumn::getColumnName).collect(Collectors.toList()); + + dbTableColumns.forEach(column -> { + if (!tableColumnNames.contains(column.getColumnName())) { + GenUtils.initColumnField(column, table); + genTableColumnMapper.insert(column); + } + }); + + List delColumns = tableColumns.stream().filter(column -> !dbTableColumnNames.contains(column.getColumnName())).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(delColumns)) { + List ids = delColumns.stream().map(GenTableColumn::getColumnId).collect(Collectors.toList()); + genTableColumnMapper.deleteBatchIds(ids); + } + } + + /** + * 批量生成代码(下载方式) + * + * @param tableNames 表数组 + * @return 数据 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream); + for (String tableName : tableNames) { + generatorCode(tableName, zip); + } + IOUtils.closeQuietly(zip); + return outputStream.toByteArray(); + } + + /** + * 查询表信息并生成代码 + */ + private void generatorCode(String tableName, ZipOutputStream zip) { + // 查询表信息 + GenTable table = baseMapper.selectGenTableByName(tableName); + // 设置主子表信息 + setSubTable(table); + // 设置主键列信息 + setPkColumn(table); + + VelocityInitializer.initVelocity(); + + VelocityContext context = VelocityUtils.prepareContext(table); + + // 获取模板列表 + List templates = VelocityUtils.getTemplateList(table.getTplCategory()); + for (String template : templates) { + // 渲染模板 + StringWriter sw = new StringWriter(); + Template tpl = Velocity.getTemplate(template, Constants.UTF8); + tpl.merge(context, sw); + try { + // 添加到zip + zip.putNextEntry(new ZipEntry(VelocityUtils.getFileName(template, table))); + IOUtils.write(sw.toString(), zip, Constants.UTF8); + IOUtils.closeQuietly(sw); + zip.flush(); + zip.closeEntry(); + } catch (IOException e) { + log.error("渲染模板失败,表名:" + table.getTableName(), e); + } + } + } + + /** + * 修改保存参数校验 + * + * @param genTable 业务信息 + */ + @Override + public void validateEdit(GenTable genTable) { + if (GenConstants.TPL_TREE.equals(genTable.getTplCategory())) { + Map paramsObj = genTable.getParams(); + if (Validator.isEmpty(paramsObj.get(GenConstants.TREE_CODE))) { + throw new CustomException("树编码字段不能为空"); + } else if (Validator.isEmpty(paramsObj.get(GenConstants.TREE_PARENT_CODE))) { + throw new CustomException("树父编码字段不能为空"); + } else if (Validator.isEmpty(paramsObj.get(GenConstants.TREE_NAME))) { + throw new CustomException("树名称字段不能为空"); + } else if (GenConstants.TPL_SUB.equals(genTable.getTplCategory())) { + if (Validator.isEmpty(genTable.getSubTableName())) { + throw new CustomException("关联子表的表名不能为空"); + } else if (Validator.isEmpty(genTable.getSubTableFkName())) { + throw new CustomException("子表关联的外键名不能为空"); + } + } + } + } + + /** + * 设置主键列信息 + * + * @param table 业务表信息 + */ + public void setPkColumn(GenTable table) { + for (GenTableColumn column : table.getColumns()) { + if (column.isPk()) { + table.setPkColumn(column); + break; + } + } + if (Validator.isNull(table.getPkColumn())) { + table.setPkColumn(table.getColumns().get(0)); + } + if (GenConstants.TPL_SUB.equals(table.getTplCategory())) { + for (GenTableColumn column : table.getSubTable().getColumns()) { + if (column.isPk()) { + table.getSubTable().setPkColumn(column); + break; + } + } + if (Validator.isNull(table.getSubTable().getPkColumn())) { + table.getSubTable().setPkColumn(table.getSubTable().getColumns().get(0)); + } + } + } + + /** + * 设置主子表信息 + * + * @param table 业务表信息 + */ + public void setSubTable(GenTable table) { + String subTableName = table.getSubTableName(); + if (Validator.isNotEmpty(subTableName)) { + table.setSubTable(baseMapper.selectGenTableByName(subTableName)); + } + } + + /** + * 设置代码生成其他选项值 + * + * @param genTable 设置后的生成对象 + */ + public void setTableFromOptions(GenTable genTable) { + Map paramsObj = JsonUtils.parseMap(genTable.getOptions()); + if (Validator.isNotNull(paramsObj)) { + String treeCode = Convert.toStr(paramsObj.get(GenConstants.TREE_CODE)); + String treeParentCode = Convert.toStr(paramsObj.get(GenConstants.TREE_PARENT_CODE)); + String treeName = Convert.toStr(paramsObj.get(GenConstants.TREE_NAME)); + String parentMenuId = Convert.toStr(paramsObj.get(GenConstants.PARENT_MENU_ID)); + String parentMenuName = Convert.toStr(paramsObj.get(GenConstants.PARENT_MENU_NAME)); + + genTable.setTreeCode(treeCode); + genTable.setTreeParentCode(treeParentCode); + genTable.setTreeName(treeName); + genTable.setParentMenuId(parentMenuId); + genTable.setParentMenuName(parentMenuName); + } + } + + /** + * 获取代码生成地址 + * + * @param table 业务表信息 + * @param template 模板文件路径 + * @return 生成地址 + */ + public static String getGenPath(GenTable table, String template) { + String genPath = table.getGenPath(); + if (StrUtil.equals(genPath, "/")) { + return System.getProperty("user.dir") + File.separator + "src" + File.separator + VelocityUtils.getFileName(template, table); + } + return genPath + File.separator + VelocityUtils.getFileName(template, table); + } +} diff --git a/bashi-generator/src/main/java/com/bashi/generator/service/IGenTableColumnService.java b/bashi-generator/src/main/java/com/bashi/generator/service/IGenTableColumnService.java new file mode 100644 index 0000000..6339370 --- /dev/null +++ b/bashi-generator/src/main/java/com/bashi/generator/service/IGenTableColumnService.java @@ -0,0 +1,45 @@ +package com.bashi.generator.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bashi.generator.domain.GenTableColumn; + +import java.util.List; + +/** + * 业务字段 服务层 + * + * @author duteliang + */ +public interface IGenTableColumnService extends IService { + /** + * 查询业务字段列表 + * + * @param tableId 业务字段编号 + * @return 业务字段集合 + */ + public List selectGenTableColumnListByTableId(Long tableId); + + /** + * 新增业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int insertGenTableColumn(GenTableColumn genTableColumn); + + /** + * 修改业务字段 + * + * @param genTableColumn 业务字段信息 + * @return 结果 + */ + public int updateGenTableColumn(GenTableColumn genTableColumn); + + /** + * 删除业务字段信息 + * + * @param ids 需要删除的数据ID + * @return 结果 + */ + public int deleteGenTableColumnByIds(String ids); +} diff --git a/bashi-generator/src/main/java/com/bashi/generator/service/IGenTableService.java b/bashi-generator/src/main/java/com/bashi/generator/service/IGenTableService.java new file mode 100644 index 0000000..a9cf235 --- /dev/null +++ b/bashi-generator/src/main/java/com/bashi/generator/service/IGenTableService.java @@ -0,0 +1,130 @@ +package com.bashi.generator.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.generator.domain.GenTable; + +import java.util.List; +import java.util.Map; + +/** + * 业务 服务层 + * + * @author duteliang + */ +public interface IGenTableService extends IService { + + + TableDataInfo selectPageGenTableList(GenTable genTable); + + + TableDataInfo selectPageDbTableList(GenTable genTable); + + /** + * 查询业务列表 + * + * @param genTable 业务信息 + * @return 业务集合 + */ + public List selectGenTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param genTable 业务信息 + * @return 数据库表集合 + */ + public List selectDbTableList(GenTable genTable); + + /** + * 查询据库列表 + * + * @param tableNames 表名称组 + * @return 数据库表集合 + */ + public List selectDbTableListByNames(String[] tableNames); + + /** + * 查询所有表信息 + * + * @return 表信息集合 + */ + public List selectGenTableAll(); + + /** + * 查询业务信息 + * + * @param id 业务ID + * @return 业务信息 + */ + public GenTable selectGenTableById(Long id); + + /** + * 修改业务 + * + * @param genTable 业务信息 + * @return 结果 + */ + public void updateGenTable(GenTable genTable); + + /** + * 删除业务信息 + * + * @param tableIds 需要删除的表数据ID + * @return 结果 + */ + public void deleteGenTableByIds(Long[] tableIds); + + /** + * 导入表结构 + * + * @param tableList 导入表列表 + */ + public void importGenTable(List tableList); + + /** + * 预览代码 + * + * @param tableId 表编号 + * @return 预览数据列表 + */ + public Map previewCode(Long tableId); + + /** + * 生成代码(下载方式) + * + * @param tableName 表名称 + * @return 数据 + */ + public byte[] downloadCode(String tableName); + + /** + * 生成代码(自定义路径) + * + * @param tableName 表名称 + * @return 数据 + */ + public void generatorCode(String tableName); + + /** + * 同步数据库 + * + * @param tableName 表名称 + */ + public void synchDb(String tableName); + + /** + * 批量生成代码(下载方式) + * + * @param tableNames 表数组 + * @return 数据 + */ + public byte[] downloadCode(String[] tableNames); + + /** + * 修改保存参数校验 + * + * @param genTable 业务信息 + */ + public void validateEdit(GenTable genTable); +} diff --git a/bashi-generator/src/main/java/com/bashi/generator/util/GenUtils.java b/bashi-generator/src/main/java/com/bashi/generator/util/GenUtils.java new file mode 100644 index 0000000..ba00be8 --- /dev/null +++ b/bashi-generator/src/main/java/com/bashi/generator/util/GenUtils.java @@ -0,0 +1,259 @@ +package com.bashi.generator.util; + +import cn.hutool.core.util.StrUtil; +import com.bashi.common.constant.GenConstants; +import com.bashi.generator.config.GenConfig; +import com.bashi.generator.domain.GenTable; +import com.bashi.generator.domain.GenTableColumn; +import org.apache.commons.lang3.RegExUtils; + +import java.util.Arrays; + +/** + * 代码生成器 工具类 + * + * @author duteliang + */ +public class GenUtils +{ + /** + * 初始化表信息 + */ + public static void initTable(GenTable genTable, String operName) + { + genTable.setClassName(convertClassName(genTable.getTableName())); + genTable.setPackageName(GenConfig.getPackageName()); + genTable.setModuleName(getModuleName(GenConfig.getPackageName())); + genTable.setBusinessName(getBusinessName(genTable.getTableName())); + genTable.setFunctionName(replaceText(genTable.getTableComment())); + genTable.setFunctionAuthor(GenConfig.getAuthor()); + genTable.setCreateBy(operName); + } + + /** + * 初始化列属性字段 + */ + public static void initColumnField(GenTableColumn column, GenTable table) + { + String dataType = getDbType(column.getColumnType()); + String columnName = column.getColumnName(); + column.setTableId(table.getTableId()); + column.setCreateBy(table.getCreateBy()); + // 设置java字段名 + column.setJavaField(StrUtil.toCamelCase(columnName)); + // 设置默认类型 + column.setJavaType(GenConstants.TYPE_STRING); + + if (arraysContains(GenConstants.COLUMNTYPE_STR, dataType) || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType)) + { + // 字符串长度超过500设置为文本域 + Integer columnLength = getColumnLength(column.getColumnType()); + String htmlType = columnLength >= 500 || arraysContains(GenConstants.COLUMNTYPE_TEXT, dataType) ? GenConstants.HTML_TEXTAREA : GenConstants.HTML_INPUT; + column.setHtmlType(htmlType); + } + else if (arraysContains(GenConstants.COLUMNTYPE_TIME, dataType)) + { + column.setJavaType(GenConstants.TYPE_DATE); + column.setHtmlType(GenConstants.HTML_DATETIME); + } + else if (arraysContains(GenConstants.COLUMNTYPE_NUMBER, dataType)) + { + column.setHtmlType(GenConstants.HTML_INPUT); + + // 如果是浮点型 统一用BigDecimal + String[] str = StrUtil.splitToArray(StrUtil.subBetween(column.getColumnType(), "(", ")"), ","); + if (str != null && str.length == 2 && Integer.parseInt(str[1]) > 0) + { + column.setJavaType(GenConstants.TYPE_BIGDECIMAL); + } + // 如果是整形 + else if (str != null && str.length == 1 && Integer.parseInt(str[0]) <= 10) + { + column.setJavaType(GenConstants.TYPE_INTEGER); + } + // 长整形 + else + { + column.setJavaType(GenConstants.TYPE_LONG); + } + } + + // 插入字段(默认所有字段都需要插入) + column.setIsInsert(GenConstants.REQUIRE); + + // 编辑字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_EDIT, columnName) && !column.isPk()) + { + column.setIsEdit(GenConstants.REQUIRE); + } + // 列表字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_LIST, columnName) && !column.isPk()) + { + column.setIsList(GenConstants.REQUIRE); + } + // 查询字段 + if (!arraysContains(GenConstants.COLUMNNAME_NOT_QUERY, columnName) && !column.isPk()) + { + column.setIsQuery(GenConstants.REQUIRE); + } + + // 查询字段类型 + if (StrUtil.endWithIgnoreCase(columnName, "name")) + { + column.setQueryType(GenConstants.QUERY_LIKE); + } + // 状态字段设置单选框 + if (StrUtil.endWithIgnoreCase(columnName, "status")) + { + column.setHtmlType(GenConstants.HTML_RADIO); + } + // 类型&性别字段设置下拉框 + else if (StrUtil.endWithIgnoreCase(columnName, "type") + || StrUtil.endWithIgnoreCase(columnName, "sex")) + { + column.setHtmlType(GenConstants.HTML_SELECT); + } + // 图片字段设置图片上传控件 + else if (StrUtil.endWithIgnoreCase(columnName, "image")) + { + column.setHtmlType(GenConstants.HTML_IMAGE_UPLOAD); + } + // 文件字段设置文件上传控件 + else if (StrUtil.endWithIgnoreCase(columnName, "file")) + { + column.setHtmlType(GenConstants.HTML_FILE_UPLOAD); + } + // 内容字段设置富文本控件 + else if (StrUtil.endWithIgnoreCase(columnName, "content")) + { + column.setHtmlType(GenConstants.HTML_EDITOR); + } + } + + /** + * 校验数组是否包含指定值 + * + * @param arr 数组 + * @param targetValue 值 + * @return 是否包含 + */ + public static boolean arraysContains(String[] arr, String targetValue) + { + return Arrays.asList(arr).contains(targetValue); + } + + /** + * 获取模块名 + * + * @param packageName 包名 + * @return 模块名 + */ + public static String getModuleName(String packageName) + { + int lastIndex = packageName.lastIndexOf("."); + int nameLength = packageName.length(); + String moduleName = StrUtil.sub(packageName, lastIndex + 1, nameLength); + return moduleName; + } + + /** + * 获取业务名 + * + * @param tableName 表名 + * @return 业务名 + */ + public static String getBusinessName(String tableName) + { + int lastIndex = tableName.lastIndexOf("_"); + int nameLength = tableName.length(); + String businessName = StrUtil.sub(tableName, lastIndex + 1, nameLength); + return businessName; + } + + /** + * 表名转换成Java类名 + * + * @param tableName 表名称 + * @return 类名 + */ + public static String convertClassName(String tableName) + { + boolean autoRemovePre = GenConfig.getAutoRemovePre(); + String tablePrefix = GenConfig.getTablePrefix(); + if (autoRemovePre && StrUtil.isNotEmpty(tablePrefix)) + { + String[] searchList = StrUtil.splitToArray(tablePrefix, ","); + tableName = replaceFirst(tableName, searchList); + } + return StrUtil.upperFirst(StrUtil.toCamelCase(tableName)); + } + + /** + * 批量替换前缀 + * + * @param replacementm 替换值 + * @param searchList 替换列表 + * @return + */ + public static String replaceFirst(String replacementm, String[] searchList) + { + String text = replacementm; + for (String searchString : searchList) + { + if (replacementm.startsWith(searchString)) + { + text = replacementm.replaceFirst(searchString, ""); + break; + } + } + return text; + } + + /** + * 关键字替换 + * + * @param text 需要被替换的名字 + * @return 替换后的名字 + */ + public static String replaceText(String text) + { + return RegExUtils.replaceAll(text, "(?:表|若依)", ""); + } + + /** + * 获取数据库类型字段 + * + * @param columnType 列类型 + * @return 截取后的列类型 + */ + public static String getDbType(String columnType) + { + if (StrUtil.indexOf(columnType, '(') > 0) + { + return StrUtil.subBefore(columnType, "(",false); + } + else + { + return columnType; + } + } + + /** + * 获取字段长度 + * + * @param columnType 列类型 + * @return 截取后的列类型 + */ + public static Integer getColumnLength(String columnType) + { + if (StrUtil.indexOf(columnType, '(') > 0) + { + String length = StrUtil.subBetween(columnType, "(", ")"); + return Integer.valueOf(length); + } + else + { + return 0; + } + } +} diff --git a/bashi-generator/src/main/java/com/bashi/generator/util/VelocityInitializer.java b/bashi-generator/src/main/java/com/bashi/generator/util/VelocityInitializer.java new file mode 100644 index 0000000..5a0f96b --- /dev/null +++ b/bashi-generator/src/main/java/com/bashi/generator/util/VelocityInitializer.java @@ -0,0 +1,35 @@ +package com.bashi.generator.util; + +import java.util.Properties; +import org.apache.velocity.app.Velocity; +import com.bashi.common.constant.Constants; + +/** + * VelocityEngine工厂 + * + * @author duteliang + */ +public class VelocityInitializer +{ + /** + * 初始化vm方法 + */ + public static void initVelocity() + { + Properties p = new Properties(); + try + { + // 加载classpath目录下的vm文件 + p.setProperty("file.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader"); + // 定义字符集 + p.setProperty(Velocity.INPUT_ENCODING, Constants.UTF8); + p.setProperty(Velocity.OUTPUT_ENCODING, Constants.UTF8); + // 初始化Velocity引擎,指定配置Properties + Velocity.init(p); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } +} diff --git a/bashi-generator/src/main/java/com/bashi/generator/util/VelocityUtils.java b/bashi-generator/src/main/java/com/bashi/generator/util/VelocityUtils.java new file mode 100644 index 0000000..4c0f138 --- /dev/null +++ b/bashi-generator/src/main/java/com/bashi/generator/util/VelocityUtils.java @@ -0,0 +1,385 @@ +package com.bashi.generator.util; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.StrUtil; +import com.bashi.common.constant.GenConstants; +import com.bashi.common.utils.DateUtils; +import com.bashi.common.utils.JsonUtils; +import com.bashi.generator.domain.GenTable; +import com.bashi.generator.domain.GenTableColumn; +import org.apache.velocity.VelocityContext; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +/** + * 模板处理工具类 + * + * @author duteliang + */ +public class VelocityUtils +{ + /** 项目空间路径 */ + private static final String PROJECT_PATH = "main/java"; + + /** mybatis空间路径 */ + private static final String MYBATIS_PATH = "main/resources/mapper"; + + /** 默认上级菜单,系统工具 */ + private static final String DEFAULT_PARENT_MENU_ID = "3"; + + /** + * 设置模板变量信息 + * + * @return 模板列表 + */ + public static VelocityContext prepareContext(GenTable genTable) + { + String moduleName = genTable.getModuleName(); + String businessName = genTable.getBusinessName(); + String packageName = genTable.getPackageName(); + String tplCategory = genTable.getTplCategory(); + String functionName = genTable.getFunctionName(); + + VelocityContext velocityContext = new VelocityContext(); + velocityContext.put("tplCategory", genTable.getTplCategory()); + velocityContext.put("tableName", genTable.getTableName()); + velocityContext.put("functionName", StrUtil.isNotEmpty(functionName) ? functionName : "【请填写功能名称】"); + velocityContext.put("ClassName", genTable.getClassName()); + velocityContext.put("className", StrUtil.lowerFirst(genTable.getClassName())); + velocityContext.put("moduleName", genTable.getModuleName()); + velocityContext.put("BusinessName", StrUtil.upperFirst(genTable.getBusinessName())); + velocityContext.put("businessName", genTable.getBusinessName()); + velocityContext.put("basePackage", getPackagePrefix(packageName)); + velocityContext.put("packageName", packageName); + velocityContext.put("author", genTable.getFunctionAuthor()); + velocityContext.put("datetime", DateUtils.getDate()); + velocityContext.put("pkColumn", genTable.getPkColumn()); + velocityContext.put("importList", getImportList(genTable)); + velocityContext.put("permissionPrefix", getPermissionPrefix(moduleName, businessName)); + velocityContext.put("columns", genTable.getColumns()); + velocityContext.put("table", genTable); + setMenuVelocityContext(velocityContext, genTable); + if (GenConstants.TPL_TREE.equals(tplCategory)) + { + setTreeVelocityContext(velocityContext, genTable); + } + if (GenConstants.TPL_SUB.equals(tplCategory)) + { + setSubVelocityContext(velocityContext, genTable); + } + return velocityContext; + } + + public static void setMenuVelocityContext(VelocityContext context, GenTable genTable) + { + String options = genTable.getOptions(); + Map paramsObj = JsonUtils.parseMap(options); + String parentMenuId = getParentMenuId(paramsObj); + context.put("parentMenuId", parentMenuId); + } + + public static void setTreeVelocityContext(VelocityContext context, GenTable genTable) + { + String options = genTable.getOptions(); + Map paramsObj = JsonUtils.parseMap(options); + String treeCode = getTreecode(paramsObj); + String treeParentCode = getTreeParentCode(paramsObj); + String treeName = getTreeName(paramsObj); + + context.put("treeCode", treeCode); + context.put("treeParentCode", treeParentCode); + context.put("treeName", treeName); + context.put("expandColumn", getExpandColumn(genTable)); + if (paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) + { + context.put("tree_parent_code", paramsObj.get(GenConstants.TREE_PARENT_CODE)); + } + if (paramsObj.containsKey(GenConstants.TREE_NAME)) + { + context.put("tree_name", paramsObj.get(GenConstants.TREE_NAME)); + } + } + + public static void setSubVelocityContext(VelocityContext context, GenTable genTable) + { + GenTable subTable = genTable.getSubTable(); + String subTableName = genTable.getSubTableName(); + String subTableFkName = genTable.getSubTableFkName(); + String subClassName = genTable.getSubTable().getClassName(); + String subTableFkClassName = StrUtil.toCamelCase(subTableFkName); + + context.put("subTable", subTable); + context.put("subTableName", subTableName); + context.put("subTableFkName", subTableFkName); + context.put("subTableFkClassName", subTableFkClassName); + context.put("subTableFkclassName", StrUtil.lowerFirst(subTableFkClassName)); + context.put("subClassName", subClassName); + context.put("subclassName", StrUtil.lowerFirst(subClassName)); + context.put("subImportList", getImportList(genTable.getSubTable())); + } + + /** + * 获取模板信息 + * + * @return 模板列表 + */ + public static List getTemplateList(String tplCategory) + { + List templates = new ArrayList(); + templates.add("vm/java/domain.java.vm"); + templates.add("vm/java/vo.java.vm"); + templates.add("vm/java/queryBo.java.vm"); + templates.add("vm/java/addBo.java.vm"); + templates.add("vm/java/editBo.java.vm"); + templates.add("vm/java/mapper.java.vm"); + templates.add("vm/java/service.java.vm"); + templates.add("vm/java/serviceImpl.java.vm"); + templates.add("vm/java/controller.java.vm"); + templates.add("vm/xml/mapper.xml.vm"); + templates.add("vm/sql/sql.vm"); + templates.add("vm/js/api.js.vm"); + if (GenConstants.TPL_CRUD.equals(tplCategory)) + { + templates.add("vm/vue/index.vue.vm"); + } + else if (GenConstants.TPL_TREE.equals(tplCategory)) + { + templates.add("vm/vue/index-tree.vue.vm"); + } + else if (GenConstants.TPL_SUB.equals(tplCategory)) + { + templates.add("vm/vue/index.vue.vm"); + templates.add("vm/java/sub-domain.java.vm"); + } + return templates; + } + + /** + * 获取文件名 + */ + public static String getFileName(String template, GenTable genTable) + { + // 文件名称 + String fileName = ""; + // 包路径 + String packageName = genTable.getPackageName(); + // 模块名 + String moduleName = genTable.getModuleName(); + // 大写类名 + String className = genTable.getClassName(); + // 业务名称 + String businessName = genTable.getBusinessName(); + + String javaPath = PROJECT_PATH + "/" + StrUtil.replace(packageName, ".", "/"); + String mybatisPath = MYBATIS_PATH + "/" + moduleName; + String vuePath = "vue"; + + if (template.contains("domain.java.vm")) + { + fileName = StrUtil.format("{}/domain/{}.java", javaPath, className); + } + if (template.contains("vo.java.vm")) + { + fileName = StrUtil.format("{}/vo/{}Vo.java", javaPath, className); + } + if (template.contains("queryBo.java.vm")) + { + fileName = StrUtil.format("{}/bo/{}QueryBo.java", javaPath, className); + } + if (template.contains("addBo.java.vm")) + { + fileName = StrUtil.format("{}/bo/{}AddBo.java", javaPath, className); + } + if (template.contains("editBo.java.vm")) + { + fileName = StrUtil.format("{}/bo/{}EditBo.java", javaPath, className); + } + if (template.contains("sub-domain.java.vm") && StrUtil.equals(GenConstants.TPL_SUB, genTable.getTplCategory())) + { + fileName = StrUtil.format("{}/domain/{}.java", javaPath, genTable.getSubTable().getClassName()); + } + else if (template.contains("mapper.java.vm")) + { + fileName = StrUtil.format("{}/mapper/{}Mapper.java", javaPath, className); + } + else if (template.contains("service.java.vm")) + { + fileName = StrUtil.format("{}/service/I{}Service.java", javaPath, className); + } + else if (template.contains("serviceImpl.java.vm")) + { + fileName = StrUtil.format("{}/service/impl/{}ServiceImpl.java", javaPath, className); + } + else if (template.contains("controller.java.vm")) + { + fileName = StrUtil.format("{}/controller/{}Controller.java", javaPath, className); + } + else if (template.contains("mapper.xml.vm")) + { + fileName = StrUtil.format("{}/{}Mapper.xml", mybatisPath, className); + } + else if (template.contains("sql.vm")) + { + fileName = businessName + "Menu.sql"; + } + else if (template.contains("api.js.vm")) + { + fileName = StrUtil.format("{}/api/{}/{}.js", vuePath, moduleName, businessName); + } + else if (template.contains("index.vue.vm")) + { + fileName = StrUtil.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName); + } + else if (template.contains("index-tree.vue.vm")) + { + fileName = StrUtil.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName); + } + return fileName; + } + + /** + * 获取包前缀 + * + * @param packageName 包名称 + * @return 包前缀名称 + */ + public static String getPackagePrefix(String packageName) + { + int lastIndex = packageName.lastIndexOf("."); + String basePackage = StrUtil.sub(packageName, 0, lastIndex); + return basePackage; + } + + /** + * 根据列类型获取导入包 + * + * @param genTable 业务表对象 + * @return 返回需要导入的包列表 + */ + public static HashSet getImportList(GenTable genTable) + { + List columns = genTable.getColumns(); + GenTable subGenTable = genTable.getSubTable(); + HashSet importList = new HashSet(); + if (Validator.isNotNull(subGenTable)) + { + importList.add("java.util.List"); + } + for (GenTableColumn column : columns) + { + if (!column.isSuperColumn() && GenConstants.TYPE_DATE.equals(column.getJavaType())) + { + importList.add("java.util.Date"); + importList.add("com.fasterxml.jackson.annotation.JsonFormat"); + } + else if (!column.isSuperColumn() && GenConstants.TYPE_BIGDECIMAL.equals(column.getJavaType())) + { + importList.add("java.math.BigDecimal"); + } + } + return importList; + } + + /** + * 获取权限前缀 + * + * @param moduleName 模块名称 + * @param businessName 业务名称 + * @return 返回权限前缀 + */ + public static String getPermissionPrefix(String moduleName, String businessName) + { + return StrUtil.format("{}:{}", moduleName, businessName); + } + + /** + * 获取上级菜单ID字段 + * + * @param paramsObj 生成其他选项 + * @return 上级菜单ID字段 + */ + public static String getParentMenuId(Map paramsObj) + { + if (Validator.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.PARENT_MENU_ID)) + { + return Convert.toStr(paramsObj.get(GenConstants.PARENT_MENU_ID)); + } + return DEFAULT_PARENT_MENU_ID; + } + + /** + * 获取树编码 + * + * @param paramsObj 生成其他选项 + * @return 树编码 + */ + public static String getTreecode(Map paramsObj) + { + if (Validator.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.TREE_CODE)) + { + return StrUtil.toCamelCase(Convert.toStr(paramsObj.get(GenConstants.TREE_CODE))); + } + return StrUtil.EMPTY; + } + + /** + * 获取树父编码 + * + * @param paramsObj 生成其他选项 + * @return 树父编码 + */ + public static String getTreeParentCode(Map paramsObj) + { + if (Validator.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.TREE_PARENT_CODE)) + { + return StrUtil.toCamelCase(Convert.toStr(paramsObj.get(GenConstants.TREE_PARENT_CODE))); + } + return StrUtil.EMPTY; + } + + /** + * 获取树名称 + * + * @param paramsObj 生成其他选项 + * @return 树名称 + */ + public static String getTreeName(Map paramsObj) + { + if (Validator.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.TREE_NAME)) + { + return StrUtil.toCamelCase(Convert.toStr(paramsObj.get(GenConstants.TREE_NAME))); + } + return StrUtil.EMPTY; + } + + /** + * 获取需要在哪一列上面显示展开按钮 + * + * @param genTable 业务表对象 + * @return 展开按钮列序号 + */ + public static int getExpandColumn(GenTable genTable) + { + String options = genTable.getOptions(); + Map paramsObj = JsonUtils.parseMap(options); + String treeName = Convert.toStr(paramsObj.get(GenConstants.TREE_NAME)); + int num = 0; + for (GenTableColumn column : genTable.getColumns()) + { + if (column.isList()) + { + num++; + String columnName = column.getColumnName(); + if (columnName.equals(treeName)) + { + break; + } + } + } + return num; + } +} diff --git a/bashi-generator/src/main/resources/generator.yml b/bashi-generator/src/main/resources/generator.yml new file mode 100644 index 0000000..70cf03e --- /dev/null +++ b/bashi-generator/src/main/resources/generator.yml @@ -0,0 +1,10 @@ +# 代码生成 +gen: + # 作者 + author: duteliang + # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool + packageName: com.bashi.system + # 自动去除表前缀,默认是false + autoRemovePre: false + # 表前缀(生成类名不会包含表前缀,多个用逗号分隔) + tablePrefix: sys_ diff --git a/bashi-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml b/bashi-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml new file mode 100644 index 0000000..d663fec --- /dev/null +++ b/bashi-generator/src/main/resources/mapper/generator/GenTableColumnMapper.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bashi-generator/src/main/resources/mapper/generator/GenTableMapper.xml b/bashi-generator/src/main/resources/mapper/generator/GenTableMapper.xml new file mode 100644 index 0000000..efe91b2 --- /dev/null +++ b/bashi-generator/src/main/resources/mapper/generator/GenTableMapper.xml @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select table_id, table_name, table_comment, sub_table_name, sub_table_fk_name, class_name, tpl_category, package_name, module_name, business_name, function_name, function_author, gen_type, gen_path, options, create_by, create_time, update_by, update_time, remark from gen_table + + + + + + + + + + + + + + + + + + + + + + diff --git a/bashi-generator/src/main/resources/vm/java/addBo.java.vm b/bashi-generator/src/main/resources/vm/java/addBo.java.vm new file mode 100644 index 0000000..82d0862 --- /dev/null +++ b/bashi-generator/src/main/resources/vm/java/addBo.java.vm @@ -0,0 +1,45 @@ +package ${packageName}.bo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import java.util.Date; +import javax.validation.constraints.*; + + +#foreach ($import in $importList) +import ${import}; +#end + +/** + * ${functionName}添加对象 ${tableName} + * + * @author ${author} + * @date ${datetime} + */ +@Data +@ApiModel("${functionName}添加对象") +public class ${ClassName}AddBo { + +#foreach ($column in $columns) +#if($column.isInsert && $column.isPk!=1) + + /** $column.columnComment */ + @ApiModelProperty("$column.columnComment") +#if($column.isRequired==1) +#if($column.javaType == 'String') + @NotBlank(message = "$column.columnComment不能为空") +#else + @NotNull(message = "$column.columnComment不能为空") +#end +#end + private $column.javaType $column.javaField; +#end +#end +#if($table.sub) + + /** $table.subTable.functionName信息 */ + @ApiModelProperty("$table.subTable.functionName") + private List<${subClassName}> ${subclassName}List; +#end +} diff --git a/bashi-generator/src/main/resources/vm/java/controller.java.vm b/bashi-generator/src/main/resources/vm/java/controller.java.vm new file mode 100644 index 0000000..497ba4c --- /dev/null +++ b/bashi-generator/src/main/resources/vm/java/controller.java.vm @@ -0,0 +1,121 @@ +package ${packageName}.controller; + +import java.util.List; + +import lombok.RequiredArgsConstructor; +import javax.validation.constraints.*; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.validation.annotation.Validated; +import com.bashi.common.annotation.RepeatSubmit; +import com.bashi.common.annotation.Log; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.enums.BusinessType; +import ${packageName}.vo.${ClassName}Vo; +import ${packageName}.bo.${ClassName}QueryBo; +import ${packageName}.bo.${ClassName}AddBo; +import ${packageName}.bo.${ClassName}EditBo; +import ${packageName}.service.I${ClassName}Service; +import com.bashi.common.utils.poi.ExcelUtil; +#if($table.crud || $table.sub) +import com.bashi.common.core.page.TableDataInfo; +#elseif($table.tree) +#end +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; + +/** + * ${functionName}Controller + * + * @author ${author} + * @date ${datetime} + */ +@Api(value = "${functionName}控制器", tags = {"${functionName}管理"}) +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@RestController +@RequestMapping("/${moduleName}/${businessName}") +public class ${ClassName}Controller extends BaseController { + + private final I${ClassName}Service i${ClassName}Service; + + /** + * 查询${functionName}列表 + */ + @ApiOperation("查询${functionName}列表") + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:list')") + @GetMapping("/list") +#if($table.crud || $table.sub) + public TableDataInfo<${ClassName}> list(PageParams pageParams, @Validated ${ClassName} bo) { + IPage<${ClassName}> page = i${ClassName}Service.page(Condition.getPage(pageParams), Wrappers.query(bo)); + return PageUtils.buildDataInfo(page); + } +#elseif($table.tree) + public AjaxResult> list(@Validated ${ClassName}QueryBo bo) { + List<${ClassName}Vo> list = i${ClassName}Service.queryList(bo); + return AjaxResult.success(list); + } +#end + + /** + * 导出${functionName}列表 + */ + @ApiOperation("导出${functionName}列表") + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:export')") + @Log(title = "${functionName}", businessType = BusinessType.EXPORT) + @GetMapping("/export") + public AjaxResult<${ClassName}Vo> export(@Validated ${ClassName}QueryBo bo) { + List<${ClassName}Vo> list = i${ClassName}Service.queryList(bo); + ExcelUtil<${ClassName}Vo> util = new ExcelUtil<${ClassName}Vo>(${ClassName}Vo.class); + return util.exportExcel(list, "${functionName}"); + } + + /** + * 获取${functionName}详细信息 + */ + @ApiOperation("获取${functionName}详细信息") + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:query')") + @GetMapping("/{${pkColumn.javaField}}") + public AjaxResult<${ClassName}> getInfo(@NotNull(message = "主键不能为空") + @PathVariable("${pkColumn.javaField}") ${pkColumn.javaType} ${pkColumn.javaField}) { + return AjaxResult.success(i${ClassName}Service.getById(${pkColumn.javaField})); + } + + /** + * 新增${functionName} + */ + @ApiOperation("新增${functionName}") + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:add')") + @Log(title = "${functionName}", businessType = BusinessType.INSERT) + @RepeatSubmit + @PostMapping() + public AjaxResult add(@Validated @RequestBody ${ClassName} bo) { + return toAjax(i${ClassName}Service.save(bo) ? 1 : 0); + } + + /** + * 修改${functionName} + */ + @ApiOperation("修改${functionName}") + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:edit')") + @Log(title = "${functionName}", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PutMapping() + public AjaxResult edit(@Validated @RequestBody ${ClassName} bo) { + return toAjax(i${ClassName}Service.updateById(bo) ? 1 : 0); + } + + /** + * 删除${functionName} + */ + @ApiOperation("删除${functionName}") + @PreAuthorize("@ss.hasPermi('${permissionPrefix}:remove')") + @Log(title = "${functionName}" , businessType = BusinessType.DELETE) + @DeleteMapping("/{${pkColumn.javaField}s}") + public AjaxResult remove(@NotEmpty(message = "主键不能为空") + @PathVariable ${pkColumn.javaType} ${pkColumn.javaField}s) { + List idList = Stream.of(ids.split(",")).collect(Collectors.toList()); + return toAjax(i${ClassName}Service.removeByIds(idList) ? 1 : 0); + } +} diff --git a/bashi-generator/src/main/resources/vm/java/domain.java.vm b/bashi-generator/src/main/resources/vm/java/domain.java.vm new file mode 100644 index 0000000..fa5889d --- /dev/null +++ b/bashi-generator/src/main/resources/vm/java/domain.java.vm @@ -0,0 +1,50 @@ +package ${packageName}.domain; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import java.io.Serializable; +import java.util.Date; +import java.math.BigDecimal; + +/** + * ${functionName}对象 ${tableName} + * + * @author ${author} + * @date ${datetime} + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +@TableName("${tableName}") +@ApiModel("${functionName}添加对象") +public class ${ClassName} implements Serializable { + + private static final long serialVersionUID=1L; + +#foreach ($column in $columns) + + /** $column.columnComment */ +#if($column.javaField=="createBy"||$column.javaField=="createTime") + @TableField(fill = FieldFill.INSERT) +#end +#if($column.javaField=="updateBy"||$column.javaField=="updateTime") + @TableField(fill = FieldFill.INSERT_UPDATE) +#end +#if($column.javaField=='delFlag') + @TableLogic +#end +#if($column.javaField=='version') + @Version +#end +#if($column.isPk==1) + @TableId(value = "$column.columnName") +#end + @ApiModelProperty("$column.columnComment") + private $column.javaType $column.javaField; +#end + +} diff --git a/bashi-generator/src/main/resources/vm/java/editBo.java.vm b/bashi-generator/src/main/resources/vm/java/editBo.java.vm new file mode 100644 index 0000000..3533a91 --- /dev/null +++ b/bashi-generator/src/main/resources/vm/java/editBo.java.vm @@ -0,0 +1,44 @@ +package ${packageName}.bo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import java.util.Date; +import javax.validation.constraints.*; + +#foreach ($import in $importList) +import ${import}; +#end + +/** + * ${functionName}编辑对象 ${tableName} + * + * @author ${author} + * @date ${datetime} + */ +@Data +@ApiModel("${functionName}编辑对象") +public class ${ClassName}EditBo { + +#foreach ($column in $columns) +#if($column.isEdit || $column.isPk==1) + + /** $column.columnComment */ + @ApiModelProperty("$column.columnComment") +#if($column.isRequired==1) +#if($column.javaType == 'String') + @NotBlank(message = "$column.columnComment不能为空") +#else + @NotNull(message = "$column.columnComment不能为空") +#end +#end + private $column.javaType $column.javaField; +#end +#end +#if($table.sub) + + /** $table.subTable.functionName信息 */ + @ApiModelProperty("$table.subTable.functionName") + private List<${subClassName}> ${subclassName}List; +#end +} diff --git a/bashi-generator/src/main/resources/vm/java/mapper.java.vm b/bashi-generator/src/main/resources/vm/java/mapper.java.vm new file mode 100644 index 0000000..80825ef --- /dev/null +++ b/bashi-generator/src/main/resources/vm/java/mapper.java.vm @@ -0,0 +1,16 @@ +package ${packageName}.mapper; + +import ${packageName}.domain.${ClassName}; +import com.bashi.common.core.mybatisplus.core.BaseMapperPlus; + +/** + * ${functionName}Mapper接口 + * + * @author ${author} + * @date ${datetime} + */ +// 如使需切换数据源 请勿使用缓存 会造成数据不一致现象 +// @CacheNamespace(implementation = MybatisPlusRedisCache.class, eviction = MybatisPlusRedisCache.class) +public interface ${ClassName}Mapper extends BaseMapperPlus<${ClassName}> { + +} diff --git a/bashi-generator/src/main/resources/vm/java/queryBo.java.vm b/bashi-generator/src/main/resources/vm/java/queryBo.java.vm new file mode 100644 index 0000000..26632d3 --- /dev/null +++ b/bashi-generator/src/main/resources/vm/java/queryBo.java.vm @@ -0,0 +1,54 @@ +package ${packageName}.bo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +#foreach ($import in $importList) +import ${import}; +#end +#if($table.crud || $table.sub) +#elseif($table.tree) +#end + +/** + * ${functionName}分页查询对象 ${tableName} + * + * @author ${author} + * @date ${datetime} + */ +#if($table.crud || $table.sub) +#set($Entity="BaseEntity") +#elseif($table.tree) +#set($Entity="TreeEntity") +#end + +@Data +@EqualsAndHashCode(callSuper = true) +@ApiModel("${functionName}分页查询对象") +public class ${ClassName}QueryBo extends ${Entity} { + + /** 分页大小 */ + @ApiModelProperty("分页大小") + private Integer pageSize; + /** 当前页数 */ + @ApiModelProperty("当前页数") + private Integer pageNum; + /** 排序列 */ + @ApiModelProperty("排序列") + private String orderByColumn; + /** 排序的方向desc或者asc */ + @ApiModelProperty(value = "排序的方向", example = "asc,desc") + private String isAsc; + + +#foreach ($column in $columns) +#if(!$table.isSuperColumn($column.javaField) && $column.query) + /** $column.columnComment */ + @ApiModelProperty("$column.columnComment") + private $column.javaType $column.javaField; +#end +#end + +} diff --git a/bashi-generator/src/main/resources/vm/java/service.java.vm b/bashi-generator/src/main/resources/vm/java/service.java.vm new file mode 100644 index 0000000..279e1c2 --- /dev/null +++ b/bashi-generator/src/main/resources/vm/java/service.java.vm @@ -0,0 +1,13 @@ +package ${packageName}.service; + +import ${packageName}.domain.${ClassName}; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + * ${functionName}Service接口 + * + * @author ${author} + * @date ${datetime} + */ +public interface I${ClassName}Service extends IService<${ClassName}> { +} diff --git a/bashi-generator/src/main/resources/vm/java/serviceImpl.java.vm b/bashi-generator/src/main/resources/vm/java/serviceImpl.java.vm new file mode 100644 index 0000000..bc65b06 --- /dev/null +++ b/bashi-generator/src/main/resources/vm/java/serviceImpl.java.vm @@ -0,0 +1,18 @@ +package ${packageName}.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import ${packageName}.domain.${ClassName}; +import ${packageName}.mapper.${ClassName}Mapper; +import ${packageName}.service.I${ClassName}Service; +import org.springframework.stereotype.Service; + +/** + * ${functionName}Service业务层处理 + * + * @author ${author} + * @date ${datetime} + */ +@Service +public class ${ClassName}ServiceImpl extends ServiceImpl<${ClassName}Mapper, ${ClassName}> implements I${ClassName}Service { + +} diff --git a/bashi-generator/src/main/resources/vm/java/sub-domain.java.vm b/bashi-generator/src/main/resources/vm/java/sub-domain.java.vm new file mode 100644 index 0000000..c431f92 --- /dev/null +++ b/bashi-generator/src/main/resources/vm/java/sub-domain.java.vm @@ -0,0 +1,73 @@ +package ${packageName}.domain; + +#foreach ($import in $subImportList) +import ${import}; +#end +import com.bashi.common.annotation.Excel; + +/** + * ${subTable.functionName}对象 ${subTableName} + * + * @author ${author} + * @date ${datetime} + */ +public class ${subClassName} extends BaseEntity +{ + private static final long serialVersionUID = 1L; + +#foreach ($column in $subTable.columns) +#if(!$table.isSuperColumn($column.javaField)) + /** $column.columnComment */ +#if($column.list) +#set($parentheseIndex=$column.columnComment.indexOf("(")) +#if($parentheseIndex != -1) +#set($comment=$column.columnComment.substring(0, $parentheseIndex)) +#else +#set($comment=$column.columnComment) +#end +#if($parentheseIndex != -1) + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") +#elseif($column.javaType == 'Date') + @JsonFormat(pattern = "yyyy-MM-dd") + @Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd") +#else + @Excel(name = "${comment}") +#end +#end + private $column.javaType $column.javaField; + +#end +#end +#foreach ($column in $subTable.columns) +#if(!$table.isSuperColumn($column.javaField)) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + public void set${AttrName}($column.javaType $column.javaField) + { + this.$column.javaField = $column.javaField; + } + + public $column.javaType get${AttrName}() + { + return $column.javaField; + } +#end +#end + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) +#foreach ($column in $subTable.columns) +#if($column.javaField.length() > 2 && $column.javaField.substring(1,2).matches("[A-Z]")) +#set($AttrName=$column.javaField) +#else +#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#end + .append("${column.javaField}", get${AttrName}()) +#end + .toString(); + } +} diff --git a/bashi-generator/src/main/resources/vm/java/vo.java.vm b/bashi-generator/src/main/resources/vm/java/vo.java.vm new file mode 100644 index 0000000..0be4edf --- /dev/null +++ b/bashi-generator/src/main/resources/vm/java/vo.java.vm @@ -0,0 +1,50 @@ +package ${packageName}.vo; + +import com.bashi.common.annotation.Excel; +#foreach ($import in $importList) +import ${import}; +#end +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + + +/** + * ${functionName}视图对象 ${tableName} + * + * @author ${author} + * @date ${datetime} + */ +@Data +@ApiModel("${functionName}视图对象") +public class ${ClassName}Vo { + + private static final long serialVersionUID = 1L; + + /** $pkColumn.columnComment */ + @ApiModelProperty("$pkColumn.columnComment") + private ${pkColumn.javaType} ${pkColumn.javaField}; + +#foreach ($column in $columns) +#if($column.isList && $column.isPk!=1) + /** $column.columnComment */ +#set($parentheseIndex=$column.columnComment.indexOf("(")) +#if($parentheseIndex != -1) +#set($comment=$column.columnComment.substring(0, $parentheseIndex)) +#else +#set($comment=$column.columnComment) +#end +#if($parentheseIndex != -1) + @Excel(name = "${comment}" , readConverterExp = "$column.readConverterExp()") +#elseif($column.javaType == 'Date') + @Excel(name = "${comment}" , width = 30, dateFormat = "yyyy-MM-dd") +#else + @Excel(name = "${comment}") +#end + @ApiModelProperty("$column.columnComment") + private $column.javaType $column.javaField; + +#end +#end + +} diff --git a/bashi-generator/src/main/resources/vm/js/api.js.vm b/bashi-generator/src/main/resources/vm/js/api.js.vm new file mode 100644 index 0000000..296d41a --- /dev/null +++ b/bashi-generator/src/main/resources/vm/js/api.js.vm @@ -0,0 +1,53 @@ +import request from '@/utils/request' + +// 查询${functionName}列表 +export function list${BusinessName}(query) { + return request({ + url: '/${moduleName}/${businessName}/list', + method: 'get', + params: query + }) +} + +// 查询${functionName}详细 +export function get${BusinessName}(${pkColumn.javaField}) { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'get' + }) +} + +// 新增${functionName} +export function add${BusinessName}(data) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'post', + data: data + }) +} + +// 修改${functionName} +export function update${BusinessName}(data) { + return request({ + url: '/${moduleName}/${businessName}', + method: 'put', + data: data + }) +} + +// 删除${functionName} +export function del${BusinessName}(${pkColumn.javaField}) { + return request({ + url: '/${moduleName}/${businessName}/' + ${pkColumn.javaField}, + method: 'delete' + }) +} + +// 导出${functionName} +export function export${BusinessName}(query) { + return request({ + url: '/${moduleName}/${businessName}/export', + method: 'get', + params: query + }) +} \ No newline at end of file diff --git a/bashi-generator/src/main/resources/vm/sql/sql.vm b/bashi-generator/src/main/resources/vm/sql/sql.vm new file mode 100644 index 0000000..0575583 --- /dev/null +++ b/bashi-generator/src/main/resources/vm/sql/sql.vm @@ -0,0 +1,22 @@ +-- 菜单 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}', '${parentMenuId}', '1', '${businessName}', '${moduleName}/${businessName}/index', 1, 0, 'C', '0', '0', '${permissionPrefix}:list', '#', 'admin', sysdate(), '', null, '${functionName}菜单'); + +-- 按钮父菜单ID +SELECT @parentId := LAST_INSERT_ID(); + +-- 按钮 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}查询', @parentId, '1', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:query', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}新增', @parentId, '2', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:add', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}修改', @parentId, '3', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:edit', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}删除', @parentId, '4', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:remove', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('${functionName}导出', @parentId, '5', '#', '', 1, 0, 'F', '0', '0', '${permissionPrefix}:export', '#', 'admin', sysdate(), '', null, ''); \ No newline at end of file diff --git a/bashi-generator/src/main/resources/vm/vue/index-tree.vue.vm b/bashi-generator/src/main/resources/vm/vue/index-tree.vue.vm new file mode 100644 index 0000000..d43c2ce --- /dev/null +++ b/bashi-generator/src/main/resources/vm/vue/index-tree.vue.vm @@ -0,0 +1,546 @@ + + + diff --git a/bashi-generator/src/main/resources/vm/vue/index.vue.vm b/bashi-generator/src/main/resources/vm/vue/index.vue.vm new file mode 100644 index 0000000..abc799e --- /dev/null +++ b/bashi-generator/src/main/resources/vm/vue/index.vue.vm @@ -0,0 +1,653 @@ + + + diff --git a/bashi-generator/src/main/resources/vm/xml/mapper.xml.vm b/bashi-generator/src/main/resources/vm/xml/mapper.xml.vm new file mode 100644 index 0000000..d053009 --- /dev/null +++ b/bashi-generator/src/main/resources/vm/xml/mapper.xml.vm @@ -0,0 +1,14 @@ + + + + + +#foreach ($column in $columns) + +#end + + + + \ No newline at end of file diff --git a/bashi-quartz/pom.xml b/bashi-quartz/pom.xml new file mode 100644 index 0000000..5c1c8f9 --- /dev/null +++ b/bashi-quartz/pom.xml @@ -0,0 +1,40 @@ + + + + bashi + com.bashi + 2.4.0 + + 4.0.0 + + bashi-quartz + + + quartz定时任务 + + + + + + + org.springframework.boot + spring-boot-starter-quartz + + + com.mchange + c3p0 + + + + + + + com.bashi + bashi-common + + + + + diff --git a/bashi-quartz/src/main/java/com/bashi/quartz/config/ScheduleConfig.java b/bashi-quartz/src/main/java/com/bashi/quartz/config/ScheduleConfig.java new file mode 100644 index 0000000..2311662 --- /dev/null +++ b/bashi-quartz/src/main/java/com/bashi/quartz/config/ScheduleConfig.java @@ -0,0 +1,13 @@ +package com.bashi.quartz.config; + +import org.springframework.context.annotation.Configuration; + +/** + * 定时任务配置 + * + * @author Lion Li + */ +@Configuration +public class ScheduleConfig { + +} diff --git a/bashi-quartz/src/main/java/com/bashi/quartz/controller/SysJobController.java b/bashi-quartz/src/main/java/com/bashi/quartz/controller/SysJobController.java new file mode 100644 index 0000000..a12f0d0 --- /dev/null +++ b/bashi-quartz/src/main/java/com/bashi/quartz/controller/SysJobController.java @@ -0,0 +1,144 @@ +package com.bashi.quartz.controller; + +import cn.hutool.core.util.StrUtil; +import com.bashi.common.annotation.Log; +import com.bashi.common.constant.Constants; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.enums.BusinessType; +import com.bashi.common.exception.job.TaskException; +import com.bashi.common.utils.SecurityUtils; +import com.bashi.common.utils.poi.ExcelUtil; +import com.bashi.quartz.domain.SysJob; +import com.bashi.quartz.service.ISysJobService; +import com.bashi.quartz.util.CronUtils; +import org.quartz.SchedulerException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 调度任务信息操作处理 + * + * @author duteliang + */ +@RestController +@RequestMapping("/monitor/job") +public class SysJobController extends BaseController +{ + @Autowired + private ISysJobService jobService; + + /** + * 查询定时任务列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:list')") + @GetMapping("/list") + public TableDataInfo list(SysJob sysJob) + { + return jobService.selectPageJobList(sysJob); + } + + /** + * 导出定时任务列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:export')") + @Log(title = "定时任务", businessType = BusinessType.EXPORT) + @GetMapping("/export") + public AjaxResult export(SysJob sysJob) + { + List list = jobService.selectJobList(sysJob); + ExcelUtil util = new ExcelUtil(SysJob.class); + return util.exportExcel(list, "定时任务"); + } + + /** + * 获取定时任务详细信息 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:query')") + @GetMapping(value = "/{jobId}") + public AjaxResult getInfo(@PathVariable("jobId") Long jobId) + { + return AjaxResult.success(jobService.selectJobById(jobId)); + } + + /** + * 新增定时任务 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:add')") + @Log(title = "定时任务", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody SysJob sysJob) throws SchedulerException, TaskException + { + if (!CronUtils.isValid(sysJob.getCronExpression())) + { + return AjaxResult.error("新增任务'" + sysJob.getJobName() + "'失败,Cron表达式不正确"); + } + else if (StrUtil.containsIgnoreCase(sysJob.getInvokeTarget(), Constants.LOOKUP_RMI)) + { + return AjaxResult.error("新增任务'" + sysJob.getJobName() + "'失败,目标字符串不允许'rmi://'调用"); + } + sysJob.setCreateBy(SecurityUtils.getUsername()); + return toAjax(jobService.insertJob(sysJob)); + } + + /** + * 修改定时任务 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:edit')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody SysJob sysJob) throws SchedulerException, TaskException + { + if (!CronUtils.isValid(sysJob.getCronExpression())) + { + return AjaxResult.error("修改任务'" + sysJob.getJobName() + "'失败,Cron表达式不正确"); + } + else if (StrUtil.containsIgnoreCase(sysJob.getInvokeTarget(), Constants.LOOKUP_RMI)) + { + return AjaxResult.error("修改任务'" + sysJob.getJobName() + "'失败,目标字符串不允许'rmi://'调用"); + } + sysJob.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(jobService.updateJob(sysJob)); + } + + /** + * 定时任务状态修改 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysJob job) throws SchedulerException + { + SysJob newJob = jobService.selectJobById(job.getJobId()); + newJob.setStatus(job.getStatus()); + return toAjax(jobService.changeStatus(newJob)); + } + + /** + * 定时任务立即执行一次 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping("/run") + public AjaxResult run(@RequestBody SysJob job) throws SchedulerException + { + jobService.run(job); + return AjaxResult.success(); + } + + /** + * 删除定时任务 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:remove')") + @Log(title = "定时任务", businessType = BusinessType.DELETE) + @DeleteMapping("/{jobIds}") + public AjaxResult remove(@PathVariable Long[] jobIds) throws SchedulerException, TaskException + { + jobService.deleteJobByIds(jobIds); + return AjaxResult.success(); + } +} diff --git a/bashi-quartz/src/main/java/com/bashi/quartz/controller/SysJobLogController.java b/bashi-quartz/src/main/java/com/bashi/quartz/controller/SysJobLogController.java new file mode 100644 index 0000000..c1916ae --- /dev/null +++ b/bashi-quartz/src/main/java/com/bashi/quartz/controller/SysJobLogController.java @@ -0,0 +1,85 @@ +package com.bashi.quartz.controller; + +import com.bashi.common.annotation.Log; +import com.bashi.common.core.controller.BaseController; +import com.bashi.common.core.domain.AjaxResult; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.enums.BusinessType; +import com.bashi.common.utils.poi.ExcelUtil; +import com.bashi.quartz.domain.SysJobLog; +import com.bashi.quartz.service.ISysJobLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 调度日志操作处理 + * + * @author duteliang + */ +@RestController +@RequestMapping("/monitor/jobLog") +public class SysJobLogController extends BaseController +{ + @Autowired + private ISysJobLogService jobLogService; + + /** + * 查询定时任务调度日志列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:list')") + @GetMapping("/list") + public TableDataInfo list(SysJobLog sysJobLog) + { + return jobLogService.selectPageJobLogList(sysJobLog); + } + + /** + * 导出定时任务调度日志列表 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:export')") + @Log(title = "任务调度日志", businessType = BusinessType.EXPORT) + @GetMapping("/export") + public AjaxResult export(SysJobLog sysJobLog) + { + List list = jobLogService.selectJobLogList(sysJobLog); + ExcelUtil util = new ExcelUtil(SysJobLog.class); + return util.exportExcel(list, "调度日志"); + } + + /** + * 根据调度编号获取详细信息 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:query')") + @GetMapping(value = "/{configId}") + public AjaxResult getInfo(@PathVariable Long jobLogId) + { + return AjaxResult.success(jobLogService.selectJobLogById(jobLogId)); + } + + + /** + * 删除定时任务调度日志 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:remove')") + @Log(title = "定时任务调度日志", businessType = BusinessType.DELETE) + @DeleteMapping("/{jobLogIds}") + public AjaxResult remove(@PathVariable Long[] jobLogIds) + { + return toAjax(jobLogService.deleteJobLogByIds(jobLogIds)); + } + + /** + * 清空定时任务调度日志 + */ + @PreAuthorize("@ss.hasPermi('monitor:job:remove')") + @Log(title = "调度日志", businessType = BusinessType.CLEAN) + @DeleteMapping("/clean") + public AjaxResult clean() + { + jobLogService.cleanJobLog(); + return AjaxResult.success(); + } +} diff --git a/bashi-quartz/src/main/java/com/bashi/quartz/domain/SysJob.java b/bashi-quartz/src/main/java/com/bashi/quartz/domain/SysJob.java new file mode 100644 index 0000000..5b6b9be --- /dev/null +++ b/bashi-quartz/src/main/java/com/bashi/quartz/domain/SysJob.java @@ -0,0 +1,134 @@ +package com.bashi.quartz.domain; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.bashi.common.annotation.Excel; +import com.bashi.common.annotation.Excel.ColumnType; +import com.bashi.common.constant.ScheduleConstants; +import com.bashi.quartz.util.CronUtils; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * 定时任务调度表 sys_job + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +@TableName("sys_job") +public class SysJob implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 任务ID + */ + @Excel(name = "任务序号", cellType = ColumnType.NUMERIC) + @TableId(value = "job_id", type = IdType.AUTO) + private Long jobId; + + /** + * 任务名称 + */ + @NotBlank(message = "任务名称不能为空") + @Size(min = 0, max = 64, message = "任务名称不能超过64个字符") + @Excel(name = "任务名称") + private String jobName; + + /** + * 任务组名 + */ + @Excel(name = "任务组名") + private String jobGroup; + + /** + * 调用目标字符串 + */ + @NotBlank(message = "调用目标字符串不能为空") + @Size(min = 0, max = 500, message = "调用目标字符串长度不能超过500个字符") + @Excel(name = "调用目标字符串") + private String invokeTarget; + + /** + * cron执行表达式 + */ + @NotBlank(message = "Cron执行表达式不能为空") + @Size(min = 0, max = 255, message = "Cron执行表达式不能超过255个字符") + @Excel(name = "执行表达式 ") + private String cronExpression; + + /** + * cron计划策略 + */ + @Excel(name = "计划策略 ", readConverterExp = "0=默认,1=立即触发执行,2=触发一次执行,3=不触发立即执行") + private String misfirePolicy = ScheduleConstants.MISFIRE_DEFAULT; + + /** + * 是否并发执行(0允许 1禁止) + */ + @Excel(name = "并发执行", readConverterExp = "0=允许,1=禁止") + private String concurrent; + + /** + * 任务状态(0正常 1暂停) + */ + @Excel(name = "任务状态", readConverterExp = "0=正常,1=暂停") + private String status; + + /** + * 创建者 + */ + @TableField(fill = FieldFill.INSERT) + private String createBy; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 更新者 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateBy; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** + * 备注 + */ + private String remark; + + /** + * 请求参数 + */ + @TableField(exist = false) + private Map params = new HashMap<>(); + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + public Date getNextValidTime() { + if (StrUtil.isNotEmpty(cronExpression)) { + return CronUtils.getNextExecution(cronExpression); + } + return null; + } + +} diff --git a/bashi-quartz/src/main/java/com/bashi/quartz/domain/SysJobLog.java b/bashi-quartz/src/main/java/com/bashi/quartz/domain/SysJobLog.java new file mode 100644 index 0000000..e523b70 --- /dev/null +++ b/bashi-quartz/src/main/java/com/bashi/quartz/domain/SysJobLog.java @@ -0,0 +1,78 @@ +package com.bashi.quartz.domain; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.bashi.common.annotation.Excel; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * 定时任务调度日志表 sys_job_log + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +@TableName("sys_job_log") +public class SysJobLog +{ + private static final long serialVersionUID = 1L; + + /** ID */ + @Excel(name = "日志序号") + @TableId(value = "job_log_id", type = IdType.AUTO) + private Long jobLogId; + + /** 任务名称 */ + @Excel(name = "任务名称") + private String jobName; + + /** 任务组名 */ + @Excel(name = "任务组名") + private String jobGroup; + + /** 调用目标字符串 */ + @Excel(name = "调用目标字符串") + private String invokeTarget; + + /** 日志信息 */ + @Excel(name = "日志信息") + private String jobMessage; + + /** 执行状态(0正常 1失败) */ + @Excel(name = "执行状态", readConverterExp = "0=正常,1=失败") + private String status; + + /** 异常信息 */ + @Excel(name = "异常信息") + private String exceptionInfo; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 请求参数 + */ + @TableField(exist = false) + private Map params = new HashMap<>(); + + /** 开始时间 */ + @TableField(exist = false) + private Date startTime; + + /** 停止时间 */ + @TableField(exist = false) + private Date stopTime; + +} diff --git a/bashi-quartz/src/main/java/com/bashi/quartz/mapper/SysJobLogMapper.java b/bashi-quartz/src/main/java/com/bashi/quartz/mapper/SysJobLogMapper.java new file mode 100644 index 0000000..b1772f7 --- /dev/null +++ b/bashi-quartz/src/main/java/com/bashi/quartz/mapper/SysJobLogMapper.java @@ -0,0 +1,13 @@ +package com.bashi.quartz.mapper; + +import com.bashi.common.core.mybatisplus.core.BaseMapperPlus; +import com.bashi.quartz.domain.SysJobLog; + +/** + * 调度任务日志信息 数据层 + * + * @author duteliang + */ +public interface SysJobLogMapper extends BaseMapperPlus { + +} diff --git a/bashi-quartz/src/main/java/com/bashi/quartz/mapper/SysJobMapper.java b/bashi-quartz/src/main/java/com/bashi/quartz/mapper/SysJobMapper.java new file mode 100644 index 0000000..8a78eea --- /dev/null +++ b/bashi-quartz/src/main/java/com/bashi/quartz/mapper/SysJobMapper.java @@ -0,0 +1,13 @@ +package com.bashi.quartz.mapper; + +import com.bashi.common.core.mybatisplus.core.BaseMapperPlus; +import com.bashi.quartz.domain.SysJob; + +/** + * 调度任务信息 数据层 + * + * @author duteliang + */ +public interface SysJobMapper extends BaseMapperPlus { + +} diff --git a/bashi-quartz/src/main/java/com/bashi/quartz/service/ISysJobLogService.java b/bashi-quartz/src/main/java/com/bashi/quartz/service/ISysJobLogService.java new file mode 100644 index 0000000..aed8ca7 --- /dev/null +++ b/bashi-quartz/src/main/java/com/bashi/quartz/service/ISysJobLogService.java @@ -0,0 +1,62 @@ +package com.bashi.quartz.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.quartz.domain.SysJobLog; + +import java.util.List; + +/** + * 定时任务调度日志信息信息 服务层 + * + * @author duteliang + */ +public interface ISysJobLogService extends IService { + + + TableDataInfo selectPageJobLogList(SysJobLog jobLog); + + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * @return 调度任务日志集合 + */ + public List selectJobLogList(SysJobLog jobLog); + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * @return 调度任务日志对象信息 + */ + public SysJobLog selectJobLogById(Long jobLogId); + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + */ + public void addJobLog(SysJobLog jobLog); + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的日志ID + * @return 结果 + */ + public int deleteJobLogByIds(Long[] logIds); + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + * @return 结果 + */ + public int deleteJobLogById(Long jobId); + + /** + * 清空任务日志 + */ + public void cleanJobLog(); +} diff --git a/bashi-quartz/src/main/java/com/bashi/quartz/service/ISysJobService.java b/bashi-quartz/src/main/java/com/bashi/quartz/service/ISysJobService.java new file mode 100644 index 0000000..8f47f12 --- /dev/null +++ b/bashi-quartz/src/main/java/com/bashi/quartz/service/ISysJobService.java @@ -0,0 +1,106 @@ +package com.bashi.quartz.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.exception.job.TaskException; +import com.bashi.quartz.domain.SysJob; +import org.quartz.SchedulerException; + +import java.util.List; + +/** + * 定时任务调度信息信息 服务层 + * + * @author duteliang + */ +public interface ISysJobService extends IService { + /** + * 获取quartz调度器的计划任务 + * + * @param job 调度信息 + * @return 调度任务集合 + */ + public TableDataInfo selectPageJobList(SysJob job); + + public List selectJobList(SysJob job); + + /** + * 通过调度任务ID查询调度信息 + * + * @param jobId 调度任务ID + * @return 调度任务对象信息 + */ + public SysJob selectJobById(Long jobId); + + /** + * 暂停任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int pauseJob(SysJob job) throws SchedulerException; + + /** + * 恢复任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int resumeJob(SysJob job) throws SchedulerException; + + /** + * 删除任务后,所对应的trigger也将被删除 + * + * @param job 调度信息 + * @return 结果 + */ + public int deleteJob(SysJob job) throws SchedulerException; + + /** + * 批量删除调度信息 + * + * @param jobIds 需要删除的任务ID + * @return 结果 + */ + public void deleteJobByIds(Long[] jobIds) throws SchedulerException; + + /** + * 任务调度状态修改 + * + * @param job 调度信息 + * @return 结果 + */ + public int changeStatus(SysJob job) throws SchedulerException; + + /** + * 立即运行任务 + * + * @param job 调度信息 + * @return 结果 + */ + public void run(SysJob job) throws SchedulerException; + + /** + * 新增任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int insertJob(SysJob job) throws SchedulerException, TaskException; + + /** + * 更新任务 + * + * @param job 调度信息 + * @return 结果 + */ + public int updateJob(SysJob job) throws SchedulerException, TaskException; + + /** + * 校验cron表达式是否有效 + * + * @param cronExpression 表达式 + * @return 结果 + */ + public boolean checkCronExpressionIsValid(String cronExpression); +} diff --git a/bashi-quartz/src/main/java/com/bashi/quartz/service/impl/SysJobLogServiceImpl.java b/bashi-quartz/src/main/java/com/bashi/quartz/service/impl/SysJobLogServiceImpl.java new file mode 100644 index 0000000..c33b3ce --- /dev/null +++ b/bashi-quartz/src/main/java/com/bashi/quartz/service/impl/SysJobLogServiceImpl.java @@ -0,0 +1,114 @@ +package com.bashi.quartz.service.impl; + +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.utils.PageUtils; +import com.bashi.quartz.domain.SysJobLog; +import com.bashi.quartz.mapper.SysJobLogMapper; +import com.bashi.quartz.service.ISysJobLogService; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * 定时任务调度日志信息 服务层 + * + * @author duteliang + */ +@Service +public class SysJobLogServiceImpl extends ServiceImpl implements ISysJobLogService { + + @Override + public TableDataInfo selectPageJobLogList(SysJobLog jobLog) { + Map params = jobLog.getParams(); + LambdaQueryWrapper lqw = new LambdaQueryWrapper() + .like(StrUtil.isNotBlank(jobLog.getJobName()), SysJobLog::getJobName, jobLog.getJobName()) + .eq(StrUtil.isNotBlank(jobLog.getJobGroup()), SysJobLog::getJobGroup, jobLog.getJobGroup()) + .eq(StrUtil.isNotBlank(jobLog.getStatus()), SysJobLog::getStatus, jobLog.getStatus()) + .like(StrUtil.isNotBlank(jobLog.getInvokeTarget()), SysJobLog::getInvokeTarget, jobLog.getInvokeTarget()) + .apply(Validator.isNotEmpty(params.get("beginTime")), + "date_format(create_time,'%y%m%d') >= date_format({0},'%y%m%d')", + params.get("beginTime")) + .apply(Validator.isNotEmpty(params.get("endTime")), + "date_format(create_time,'%y%m%d') <= date_format({0},'%y%m%d')", + params.get("endTime")); + return PageUtils.buildDataInfo(page(PageUtils.buildPage(), lqw)); + } + + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * @return 调度任务日志集合 + */ + @Override + public List selectJobLogList(SysJobLog jobLog) { + Map params = jobLog.getParams(); + return list(new LambdaQueryWrapper() + .like(StrUtil.isNotBlank(jobLog.getJobName()), SysJobLog::getJobName, jobLog.getJobName()) + .eq(StrUtil.isNotBlank(jobLog.getJobGroup()), SysJobLog::getJobGroup, jobLog.getJobGroup()) + .eq(StrUtil.isNotBlank(jobLog.getStatus()), SysJobLog::getStatus, jobLog.getStatus()) + .like(StrUtil.isNotBlank(jobLog.getInvokeTarget()), SysJobLog::getInvokeTarget, jobLog.getInvokeTarget()) + .apply(Validator.isNotEmpty(params.get("beginTime")), + "date_format(create_time,'%y%m%d') >= date_format({0},'%y%m%d')", + params.get("beginTime")) + .apply(Validator.isNotEmpty(params.get("endTime")), + "date_format(create_time,'%y%m%d') <= date_format({0},'%y%m%d')", + params.get("endTime"))); + } + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * @return 调度任务日志对象信息 + */ + @Override + public SysJobLog selectJobLogById(Long jobLogId) { + return getById(jobLogId); + } + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + */ + @Override + public void addJobLog(SysJobLog jobLog) { + baseMapper.insert(jobLog); + } + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的数据ID + * @return 结果 + */ + @Override + public int deleteJobLogByIds(Long[] logIds) { + return baseMapper.deleteBatchIds(Arrays.asList(logIds)); + } + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + */ + @Override + public int deleteJobLogById(Long jobId) { + return baseMapper.deleteById(jobId); + } + + /** + * 清空任务日志 + */ + @Override + public void cleanJobLog() { + remove(new LambdaQueryWrapper<>()); + } +} diff --git a/bashi-quartz/src/main/java/com/bashi/quartz/service/impl/SysJobServiceImpl.java b/bashi-quartz/src/main/java/com/bashi/quartz/service/impl/SysJobServiceImpl.java new file mode 100644 index 0000000..5d3e481 --- /dev/null +++ b/bashi-quartz/src/main/java/com/bashi/quartz/service/impl/SysJobServiceImpl.java @@ -0,0 +1,246 @@ +package com.bashi.quartz.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bashi.common.constant.ScheduleConstants; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.exception.job.TaskException; +import com.bashi.common.utils.PageUtils; +import com.bashi.quartz.domain.SysJob; +import com.bashi.quartz.mapper.SysJobMapper; +import com.bashi.quartz.service.ISysJobService; +import com.bashi.quartz.util.CronUtils; +import com.bashi.quartz.util.ScheduleUtils; +import org.quartz.JobDataMap; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.PostConstruct; +import java.util.List; + +/** + * 定时任务调度信息 服务层 + * + * @author duteliang + */ +@Service +public class SysJobServiceImpl extends ServiceImpl implements ISysJobService { + @Autowired + private Scheduler scheduler; + + /** + * 项目启动时,初始化定时器 主要是防止手动修改数据库导致未同步到定时任务处理(注:不能手动修改数据库ID和任务组名,否则会导致脏数据) + */ + @PostConstruct + public void init() throws SchedulerException, TaskException { + scheduler.clear(); + List jobList = list(); + for (SysJob job : jobList) { + ScheduleUtils.createScheduleJob(scheduler, job); + } + } + + @Override + public TableDataInfo selectPageJobList(SysJob job) { + LambdaQueryWrapper lqw = new LambdaQueryWrapper() + .like(StrUtil.isNotBlank(job.getJobName()), SysJob::getJobName, job.getJobName()) + .eq(StrUtil.isNotBlank(job.getJobGroup()), SysJob::getJobGroup, job.getJobGroup()) + .eq(StrUtil.isNotBlank(job.getStatus()), SysJob::getStatus, job.getStatus()) + .like(StrUtil.isNotBlank(job.getInvokeTarget()), SysJob::getInvokeTarget, job.getInvokeTarget()); + return PageUtils.buildDataInfo(page(PageUtils.buildPage(), lqw)); + } + + /** + * 获取quartz调度器的计划任务列表 + * + * @param job 调度信息 + * @return + */ + @Override + public List selectJobList(SysJob job) { + return list(new LambdaQueryWrapper() + .like(StrUtil.isNotBlank(job.getJobName()), SysJob::getJobName, job.getJobName()) + .eq(StrUtil.isNotBlank(job.getJobGroup()), SysJob::getJobGroup, job.getJobGroup()) + .eq(StrUtil.isNotBlank(job.getStatus()), SysJob::getStatus, job.getStatus()) + .like(StrUtil.isNotBlank(job.getInvokeTarget()), SysJob::getInvokeTarget, job.getInvokeTarget())); + } + + /** + * 通过调度任务ID查询调度信息 + * + * @param jobId 调度任务ID + * @return 调度任务对象信息 + */ + @Override + public SysJob selectJobById(Long jobId) { + return getById(jobId); + } + + /** + * 暂停任务 + * + * @param job 调度信息 + */ + @Override + @Transactional + public int pauseJob(SysJob job) throws SchedulerException { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + job.setStatus(ScheduleConstants.Status.PAUSE.getValue()); + int rows = baseMapper.updateById(job); + if (rows > 0) { + scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 恢复任务 + * + * @param job 调度信息 + */ + @Override + @Transactional + public int resumeJob(SysJob job) throws SchedulerException { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + job.setStatus(ScheduleConstants.Status.NORMAL.getValue()); + int rows = baseMapper.updateById(job); + if (rows > 0) { + scheduler.resumeJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 删除任务后,所对应的trigger也将被删除 + * + * @param job 调度信息 + */ + @Override + @Transactional + public int deleteJob(SysJob job) throws SchedulerException { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + int rows = baseMapper.deleteById(jobId); + if (rows > 0) { + scheduler.deleteJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 批量删除调度信息 + * + * @param jobIds 需要删除的任务ID + * @return 结果 + */ + @Override + @Transactional + public void deleteJobByIds(Long[] jobIds) throws SchedulerException { + for (Long jobId : jobIds) { + SysJob job = getById(jobId); + deleteJob(job); + } + } + + /** + * 任务调度状态修改 + * + * @param job 调度信息 + */ + @Override + @Transactional + public int changeStatus(SysJob job) throws SchedulerException { + int rows = 0; + String status = job.getStatus(); + if (ScheduleConstants.Status.NORMAL.getValue().equals(status)) { + rows = resumeJob(job); + } else if (ScheduleConstants.Status.PAUSE.getValue().equals(status)) { + rows = pauseJob(job); + } + return rows; + } + + /** + * 立即运行任务 + * + * @param job 调度信息 + */ + @Override + @Transactional + public void run(SysJob job) throws SchedulerException { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + SysJob properties = selectJobById(job.getJobId()); + // 参数 + JobDataMap dataMap = new JobDataMap(); + dataMap.put(ScheduleConstants.TASK_PROPERTIES, properties); + scheduler.triggerJob(ScheduleUtils.getJobKey(jobId, jobGroup), dataMap); + } + + /** + * 新增任务 + * + * @param job 调度信息 调度信息 + */ + @Override + @Transactional + public int insertJob(SysJob job) throws SchedulerException, TaskException { + job.setStatus(ScheduleConstants.Status.PAUSE.getValue()); + int rows = baseMapper.insert(job); + if (rows > 0) { + ScheduleUtils.createScheduleJob(scheduler, job); + } + return rows; + } + + /** + * 更新任务的时间表达式 + * + * @param job 调度信息 + */ + @Override + @Transactional + public int updateJob(SysJob job) throws SchedulerException, TaskException { + SysJob properties = selectJobById(job.getJobId()); + int rows = baseMapper.updateById(job); + if (rows > 0) { + updateSchedulerJob(job, properties.getJobGroup()); + } + return rows; + } + + /** + * 更新任务 + * + * @param job 任务对象 + * @param jobGroup 任务组名 + */ + public void updateSchedulerJob(SysJob job, String jobGroup) throws SchedulerException, TaskException { + Long jobId = job.getJobId(); + // 判断是否存在 + JobKey jobKey = ScheduleUtils.getJobKey(jobId, jobGroup); + if (scheduler.checkExists(jobKey)) { + // 防止创建时存在数据问题 先移除,然后在执行创建操作 + scheduler.deleteJob(jobKey); + } + ScheduleUtils.createScheduleJob(scheduler, job); + } + + /** + * 校验cron表达式是否有效 + * + * @param cronExpression 表达式 + * @return 结果 + */ + @Override + public boolean checkCronExpressionIsValid(String cronExpression) { + return CronUtils.isValid(cronExpression); + } +} diff --git a/bashi-quartz/src/main/java/com/bashi/quartz/task/RyTask.java b/bashi-quartz/src/main/java/com/bashi/quartz/task/RyTask.java new file mode 100644 index 0000000..48243fb --- /dev/null +++ b/bashi-quartz/src/main/java/com/bashi/quartz/task/RyTask.java @@ -0,0 +1,29 @@ +package com.bashi.quartz.task; + +import cn.hutool.core.lang.Console; +import cn.hutool.core.util.StrUtil; +import org.springframework.stereotype.Component; + +/** + * 定时任务调度测试 + * + * @author duteliang + */ +@Component("ryTask") +public class RyTask +{ + public void ryMultipleParams(String s, Boolean b, Long l, Double d, Integer i) + { + Console.log(StrUtil.format("执行多参方法: 字符串类型{},布尔类型{},长整型{},浮点型{},整形{}", s, b, l, d, i)); + } + + public void ryParams(String params) + { + Console.log("执行有参方法:" + params); + } + + public void ryNoParams() + { + Console.log("执行无参方法"); + } +} diff --git a/bashi-quartz/src/main/java/com/bashi/quartz/util/AbstractQuartzJob.java b/bashi-quartz/src/main/java/com/bashi/quartz/util/AbstractQuartzJob.java new file mode 100644 index 0000000..e204432 --- /dev/null +++ b/bashi-quartz/src/main/java/com/bashi/quartz/util/AbstractQuartzJob.java @@ -0,0 +1,109 @@ +package com.bashi.quartz.util; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.StrUtil; +import com.bashi.common.constant.Constants; +import com.bashi.common.constant.ScheduleConstants; +import com.bashi.common.utils.spring.SpringUtils; +import com.bashi.quartz.domain.SysJob; +import com.bashi.quartz.domain.SysJobLog; +import com.bashi.quartz.service.ISysJobLogService; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; + +/** + * 抽象quartz调用 + * + * @author duteliang + */ +public abstract class AbstractQuartzJob implements Job +{ + private static final Logger log = LoggerFactory.getLogger(AbstractQuartzJob.class); + + /** + * 线程本地变量 + */ + private static ThreadLocal threadLocal = new ThreadLocal<>(); + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException + { + SysJob sysJob = new SysJob(); + BeanUtil.copyProperties(context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES),sysJob); + try + { + before(context, sysJob); + if (Validator.isNotNull(sysJob)) + { + doExecute(context, sysJob); + } + after(context, sysJob, null); + } + catch (Exception e) + { + log.error("任务执行异常 - :", e); + after(context, sysJob, e); + } + } + + /** + * 执行前 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + */ + protected void before(JobExecutionContext context, SysJob sysJob) + { + threadLocal.set(new Date()); + } + + /** + * 执行后 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + */ + protected void after(JobExecutionContext context, SysJob sysJob, Exception e) + { + Date startTime = threadLocal.get(); + threadLocal.remove(); + + final SysJobLog sysJobLog = new SysJobLog(); + sysJobLog.setJobName(sysJob.getJobName()); + sysJobLog.setJobGroup(sysJob.getJobGroup()); + sysJobLog.setInvokeTarget(sysJob.getInvokeTarget()); + sysJobLog.setStartTime(startTime); + sysJobLog.setStopTime(new Date()); + long runMs = sysJobLog.getStopTime().getTime() - sysJobLog.getStartTime().getTime(); + sysJobLog.setJobMessage(sysJobLog.getJobName() + " 总共耗时:" + runMs + "毫秒"); + if (e != null) + { + sysJobLog.setStatus(Constants.FAIL); + String errorMsg = StrUtil.sub(ExceptionUtil.stacktraceToString(e), 0, 2000); + sysJobLog.setExceptionInfo(errorMsg); + } + else + { + sysJobLog.setStatus(Constants.SUCCESS); + } + + // 写入数据库当中 + SpringUtils.getBean(ISysJobLogService.class).addJobLog(sysJobLog); + } + + /** + * 执行方法,由子类重载 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + * @throws Exception 执行过程中的异常 + */ + protected abstract void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception; +} diff --git a/bashi-quartz/src/main/java/com/bashi/quartz/util/CronUtils.java b/bashi-quartz/src/main/java/com/bashi/quartz/util/CronUtils.java new file mode 100644 index 0000000..48ea060 --- /dev/null +++ b/bashi-quartz/src/main/java/com/bashi/quartz/util/CronUtils.java @@ -0,0 +1,63 @@ +package com.bashi.quartz.util; + +import java.text.ParseException; +import java.util.Date; +import org.quartz.CronExpression; + +/** + * cron表达式工具类 + * + * @author duteliang + * + */ +public class CronUtils +{ + /** + * 返回一个布尔值代表一个给定的Cron表达式的有效性 + * + * @param cronExpression Cron表达式 + * @return boolean 表达式是否有效 + */ + public static boolean isValid(String cronExpression) + { + return CronExpression.isValidExpression(cronExpression); + } + + /** + * 返回一个字符串值,表示该消息无效Cron表达式给出有效性 + * + * @param cronExpression Cron表达式 + * @return String 无效时返回表达式错误描述,如果有效返回null + */ + public static String getInvalidMessage(String cronExpression) + { + try + { + new CronExpression(cronExpression); + return null; + } + catch (ParseException pe) + { + return pe.getMessage(); + } + } + + /** + * 返回下一个执行时间根据给定的Cron表达式 + * + * @param cronExpression Cron表达式 + * @return Date 下次Cron表达式执行时间 + */ + public static Date getNextExecution(String cronExpression) + { + try + { + CronExpression cron = new CronExpression(cronExpression); + return cron.getNextValidTimeAfter(new Date(System.currentTimeMillis())); + } + catch (ParseException e) + { + throw new IllegalArgumentException(e.getMessage()); + } + } +} diff --git a/bashi-quartz/src/main/java/com/bashi/quartz/util/JobInvokeUtil.java b/bashi-quartz/src/main/java/com/bashi/quartz/util/JobInvokeUtil.java new file mode 100644 index 0000000..8d64591 --- /dev/null +++ b/bashi-quartz/src/main/java/com/bashi/quartz/util/JobInvokeUtil.java @@ -0,0 +1,184 @@ +package com.bashi.quartz.util; + +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.StrUtil; +import com.bashi.common.utils.spring.SpringUtils; +import com.bashi.quartz.domain.SysJob; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.LinkedList; +import java.util.List; + +/** + * 任务执行工具 + * + * @author duteliang + */ +public class JobInvokeUtil +{ + /** + * 执行方法 + * + * @param sysJob 系统任务 + */ + public static void invokeMethod(SysJob sysJob) throws Exception + { + String invokeTarget = sysJob.getInvokeTarget(); + String beanName = getBeanName(invokeTarget); + String methodName = getMethodName(invokeTarget); + List methodParams = getMethodParams(invokeTarget); + + if (!isValidClassName(beanName)) + { + Object bean = SpringUtils.getBean(beanName); + invokeMethod(bean, methodName, methodParams); + } + else + { + Object bean = Class.forName(beanName).newInstance(); + invokeMethod(bean, methodName, methodParams); + } + } + + /** + * 调用任务方法 + * + * @param bean 目标对象 + * @param methodName 方法名称 + * @param methodParams 方法参数 + */ + private static void invokeMethod(Object bean, String methodName, List methodParams) + throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, + InvocationTargetException + { + if (Validator.isNotNull(methodParams) && methodParams.size() > 0) + { + Method method = bean.getClass().getDeclaredMethod(methodName, getMethodParamsType(methodParams)); + method.invoke(bean, getMethodParamsValue(methodParams)); + } + else + { + Method method = bean.getClass().getDeclaredMethod(methodName); + method.invoke(bean); + } + } + + /** + * 校验是否为为class包名 + * + * @param str 名称 + * @return true是 false否 + */ + public static boolean isValidClassName(String invokeTarget) + { + return StrUtil.count(invokeTarget, ".") > 1; + } + + /** + * 获取bean名称 + * + * @param invokeTarget 目标字符串 + * @return bean名称 + */ + public static String getBeanName(String invokeTarget) + { + String beanName = StrUtil.subBefore(invokeTarget, "(",false); + return StrUtil.subBefore(beanName, ".",true); + } + + /** + * 获取bean方法 + * + * @param invokeTarget 目标字符串 + * @return method方法 + */ + public static String getMethodName(String invokeTarget) + { + String methodName = StrUtil.subBefore(invokeTarget, "(",false); + return StrUtil.subAfter(methodName, ".",true); + } + + /** + * 获取method方法参数相关列表 + * + * @param invokeTarget 目标字符串 + * @return method方法相关参数列表 + */ + public static List getMethodParams(String invokeTarget) + { + String methodStr = StrUtil.subBetween(invokeTarget, "(", ")"); + if (StrUtil.isEmpty(methodStr)) + { + return null; + } + String[] methodParams = methodStr.split(","); + List classs = new LinkedList<>(); + for (int i = 0; i < methodParams.length; i++) + { + String str = StrUtil.trimToEmpty(methodParams[i]); + // String字符串类型,包含' + if (StrUtil.contains(str, "'")) + { + classs.add(new Object[] { StrUtil.replace(str, "'", ""), String.class }); + } + // boolean布尔类型,等于true或者false + else if (StrUtil.equals(str, "true") || StrUtil.equalsIgnoreCase(str, "false")) + { + classs.add(new Object[] { Boolean.valueOf(str), Boolean.class }); + } + // long长整形,包含L + else if (StrUtil.containsIgnoreCase(str, "L")) + { + classs.add(new Object[] { Long.valueOf(StrUtil.replaceIgnoreCase(str, "L", "")), Long.class }); + } + // double浮点类型,包含D + else if (StrUtil.containsIgnoreCase(str, "D")) + { + classs.add(new Object[] { Double.valueOf(StrUtil.replaceIgnoreCase(str, "D", "")), Double.class }); + } + // 其他类型归类为整形 + else + { + classs.add(new Object[] { Integer.valueOf(str), Integer.class }); + } + } + return classs; + } + + /** + * 获取参数类型 + * + * @param methodParams 参数相关列表 + * @return 参数类型列表 + */ + public static Class[] getMethodParamsType(List methodParams) + { + Class[] classs = new Class[methodParams.size()]; + int index = 0; + for (Object[] os : methodParams) + { + classs[index] = (Class) os[1]; + index++; + } + return classs; + } + + /** + * 获取参数值 + * + * @param methodParams 参数相关列表 + * @return 参数值列表 + */ + public static Object[] getMethodParamsValue(List methodParams) + { + Object[] classs = new Object[methodParams.size()]; + int index = 0; + for (Object[] os : methodParams) + { + classs[index] = (Object) os[0]; + index++; + } + return classs; + } +} diff --git a/bashi-quartz/src/main/java/com/bashi/quartz/util/QuartzDisallowConcurrentExecution.java b/bashi-quartz/src/main/java/com/bashi/quartz/util/QuartzDisallowConcurrentExecution.java new file mode 100644 index 0000000..10599e8 --- /dev/null +++ b/bashi-quartz/src/main/java/com/bashi/quartz/util/QuartzDisallowConcurrentExecution.java @@ -0,0 +1,21 @@ +package com.bashi.quartz.util; + +import org.quartz.DisallowConcurrentExecution; +import org.quartz.JobExecutionContext; +import com.bashi.quartz.domain.SysJob; + +/** + * 定时任务处理(禁止并发执行) + * + * @author duteliang + * + */ +@DisallowConcurrentExecution +public class QuartzDisallowConcurrentExecution extends AbstractQuartzJob +{ + @Override + protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception + { + JobInvokeUtil.invokeMethod(sysJob); + } +} diff --git a/bashi-quartz/src/main/java/com/bashi/quartz/util/QuartzJobExecution.java b/bashi-quartz/src/main/java/com/bashi/quartz/util/QuartzJobExecution.java new file mode 100644 index 0000000..28efa11 --- /dev/null +++ b/bashi-quartz/src/main/java/com/bashi/quartz/util/QuartzJobExecution.java @@ -0,0 +1,19 @@ +package com.bashi.quartz.util; + +import org.quartz.JobExecutionContext; +import com.bashi.quartz.domain.SysJob; + +/** + * 定时任务处理(允许并发执行) + * + * @author duteliang + * + */ +public class QuartzJobExecution extends AbstractQuartzJob +{ + @Override + protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception + { + JobInvokeUtil.invokeMethod(sysJob); + } +} diff --git a/bashi-quartz/src/main/java/com/bashi/quartz/util/ScheduleUtils.java b/bashi-quartz/src/main/java/com/bashi/quartz/util/ScheduleUtils.java new file mode 100644 index 0000000..0410607 --- /dev/null +++ b/bashi-quartz/src/main/java/com/bashi/quartz/util/ScheduleUtils.java @@ -0,0 +1,113 @@ +package com.bashi.quartz.util; + +import org.quartz.CronScheduleBuilder; +import org.quartz.CronTrigger; +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.TriggerBuilder; +import org.quartz.TriggerKey; +import com.bashi.common.constant.ScheduleConstants; +import com.bashi.common.exception.job.TaskException; +import com.bashi.common.exception.job.TaskException.Code; +import com.bashi.quartz.domain.SysJob; + +/** + * 定时任务工具类 + * + * @author duteliang + * + */ +public class ScheduleUtils +{ + /** + * 得到quartz任务类 + * + * @param sysJob 执行计划 + * @return 具体执行任务类 + */ + private static Class getQuartzJobClass(SysJob sysJob) + { + boolean isConcurrent = "0".equals(sysJob.getConcurrent()); + return isConcurrent ? QuartzJobExecution.class : QuartzDisallowConcurrentExecution.class; + } + + /** + * 构建任务触发对象 + */ + public static TriggerKey getTriggerKey(Long jobId, String jobGroup) + { + return TriggerKey.triggerKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup); + } + + /** + * 构建任务键对象 + */ + public static JobKey getJobKey(Long jobId, String jobGroup) + { + return JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup); + } + + /** + * 创建定时任务 + */ + public static void createScheduleJob(Scheduler scheduler, SysJob job) throws SchedulerException, TaskException + { + Class jobClass = getQuartzJobClass(job); + // 构建job信息 + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(getJobKey(jobId, jobGroup)).build(); + + // 表达式调度构建器 + CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression()); + cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder); + + // 按新的cronExpression表达式构建一个新的trigger + CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(jobId, jobGroup)) + .withSchedule(cronScheduleBuilder).build(); + + // 放入参数,运行时的方法可以获取 + jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job); + + // 判断是否存在 + if (scheduler.checkExists(getJobKey(jobId, jobGroup))) + { + // 防止创建时存在数据问题 先移除,然后在执行创建操作 + scheduler.deleteJob(getJobKey(jobId, jobGroup)); + } + + scheduler.scheduleJob(jobDetail, trigger); + + // 暂停任务 + if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue())) + { + scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + } + + /** + * 设置定时任务策略 + */ + public static CronScheduleBuilder handleCronScheduleMisfirePolicy(SysJob job, CronScheduleBuilder cb) + throws TaskException + { + switch (job.getMisfirePolicy()) + { + case ScheduleConstants.MISFIRE_DEFAULT: + return cb; + case ScheduleConstants.MISFIRE_IGNORE_MISFIRES: + return cb.withMisfireHandlingInstructionIgnoreMisfires(); + case ScheduleConstants.MISFIRE_FIRE_AND_PROCEED: + return cb.withMisfireHandlingInstructionFireAndProceed(); + case ScheduleConstants.MISFIRE_DO_NOTHING: + return cb.withMisfireHandlingInstructionDoNothing(); + default: + throw new TaskException("The task misfire policy '" + job.getMisfirePolicy() + + "' cannot be used in cron schedule tasks", Code.CONFIG_ERROR); + } + } +} diff --git a/bashi-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml b/bashi-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml new file mode 100644 index 0000000..e6cdc57 --- /dev/null +++ b/bashi-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/bashi-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml b/bashi-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml new file mode 100644 index 0000000..430bb93 --- /dev/null +++ b/bashi-quartz/src/main/resources/mapper/quartz/SysJobMapper.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/bashi-system/pom.xml b/bashi-system/pom.xml new file mode 100644 index 0000000..6781728 --- /dev/null +++ b/bashi-system/pom.xml @@ -0,0 +1,28 @@ + + + + bashi + com.bashi + 2.4.0 + + 4.0.0 + + bashi-system + + + system系统模块 + + + + + + + com.bashi + bashi-common + + + + + diff --git a/bashi-system/src/main/java/com/bashi/system/domain/SysConfig.java b/bashi-system/src/main/java/com/bashi/system/domain/SysConfig.java new file mode 100644 index 0000000..c29c087 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/domain/SysConfig.java @@ -0,0 +1,105 @@ +package com.bashi.system.domain; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.bashi.common.annotation.Excel; +import com.bashi.common.annotation.Excel.ColumnType; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * 参数配置表 sys_config + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +@TableName("sys_config") +public class SysConfig implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 参数主键 + */ + @Excel(name = "参数主键", cellType = ColumnType.NUMERIC) + @TableId(value = "config_id", type = IdType.AUTO) + private Long configId; + + /** + * 参数名称 + */ + @Excel(name = "参数名称") + @NotBlank(message = "参数名称不能为空") + @Size(min = 0, max = 100, message = "参数名称不能超过100个字符") + private String configName; + + /** + * 参数键名 + */ + @Excel(name = "参数键名") + @NotBlank(message = "参数键名长度不能为空") + @Size(min = 0, max = 100, message = "参数键名长度不能超过100个字符") + private String configKey; + + /** + * 参数键值 + */ + @Excel(name = "参数键值") + @NotBlank(message = "参数键值不能为空") + @Size(min = 0, max = 500, message = "参数键值长度不能超过500个字符") + private String configValue; + + /** + * 系统内置(Y是 N否) + */ + @Excel(name = "系统内置", readConverterExp = "Y=是,N=否") + private String configType; + + /** + * 创建者 + */ + @TableField(fill = FieldFill.INSERT) + private String createBy; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 更新者 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateBy; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** + * 备注 + */ + private String remark; + + /** + * 请求参数 + */ + @TableField(exist = false) + private Map params = new HashMap<>(); + +} diff --git a/bashi-system/src/main/java/com/bashi/system/domain/SysLogininfor.java b/bashi-system/src/main/java/com/bashi/system/domain/SysLogininfor.java new file mode 100644 index 0000000..87c2e77 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/domain/SysLogininfor.java @@ -0,0 +1,94 @@ +package com.bashi.system.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.bashi.common.annotation.Excel; +import com.bashi.common.annotation.Excel.ColumnType; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * 系统访问记录表 sys_logininfor + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +@TableName("sys_logininfor") +public class SysLogininfor implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Excel(name = "序号", cellType = ColumnType.NUMERIC) + @TableId(value = "info_id", type = IdType.AUTO) + private Long infoId; + + /** + * 用户账号 + */ + @Excel(name = "用户账号") + private String userName; + + /** + * 登录状态 0成功 1失败 + */ + @Excel(name = "登录状态", readConverterExp = "0=成功,1=失败") + private String status; + + /** + * 登录IP地址 + */ + @Excel(name = "登录地址") + private String ipaddr; + + /** + * 登录地点 + */ + @Excel(name = "登录地点") + private String loginLocation; + + /** + * 浏览器类型 + */ + @Excel(name = "浏览器") + private String browser; + + /** + * 操作系统 + */ + @Excel(name = "操作系统") + private String os; + + /** + * 提示消息 + */ + @Excel(name = "提示消息") + private String msg; + + /** + * 访问时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "访问时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date loginTime; + + /** + * 请求参数 + */ + @TableField(exist = false) + private Map params = new HashMap<>(); + +} diff --git a/bashi-system/src/main/java/com/bashi/system/domain/SysNotice.java b/bashi-system/src/main/java/com/bashi/system/domain/SysNotice.java new file mode 100644 index 0000000..769fae0 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/domain/SysNotice.java @@ -0,0 +1,93 @@ +package com.bashi.system.domain; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * 通知公告表 sys_notice + * + * @author duteliang + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +@TableName("sys_notice") +public class SysNotice implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 公告ID + */ + @TableId(value = "notice_id", type = IdType.AUTO) + private Long noticeId; + + /** + * 公告标题 + */ + @NotBlank(message = "公告标题不能为空") + @Size(min = 0, max = 50, message = "公告标题不能超过50个字符") + private String noticeTitle; + + /** + * 公告类型(1通知 2公告) + */ + private String noticeType; + + /** + * 公告内容 + */ + private String noticeContent; + + /** + * 公告状态(0正常 1关闭) + */ + private String status; + + /** + * 创建者 + */ + @TableField(fill = FieldFill.INSERT) + private String createBy; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 更新者 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateBy; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** + * 备注 + */ + private String remark; + + /** + * 请求参数 + */ + @TableField(exist = false) + private Map params = new HashMap<>(); + +} diff --git a/bashi-system/src/main/java/com/bashi/system/domain/SysOperLog.java b/bashi-system/src/main/java/com/bashi/system/domain/SysOperLog.java new file mode 100644 index 0000000..026fb8e --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/domain/SysOperLog.java @@ -0,0 +1,142 @@ +package com.bashi.system.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.bashi.common.annotation.Excel; +import com.bashi.common.annotation.Excel.ColumnType; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * 操作日志记录表 oper_log + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +@TableName("sys_oper_log") +public class SysOperLog implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 日志主键 + */ + @Excel(name = "操作序号", cellType = ColumnType.NUMERIC) + @TableId(value = "oper_id", type = IdType.AUTO) + private Long operId; + + /** + * 操作模块 + */ + @Excel(name = "操作模块") + private String title; + + /** + * 业务类型(0其它 1新增 2修改 3删除) + */ + @Excel(name = "业务类型", readConverterExp = "0=其它,1=新增,2=修改,3=删除,4=授权,5=导出,6=导入,7=强退,8=生成代码,9=清空数据") + private Integer businessType; + + /** + * 业务类型数组 + */ + @TableField(exist = false) + private Integer[] businessTypes; + + /** + * 请求方法 + */ + @Excel(name = "请求方法") + private String method; + + /** + * 请求方式 + */ + @Excel(name = "请求方式") + private String requestMethod; + + /** + * 操作类别(0其它 1后台用户 2手机端用户) + */ + @Excel(name = "操作类别", readConverterExp = "0=其它,1=后台用户,2=手机端用户") + private Integer operatorType; + + /** + * 操作人员 + */ + @Excel(name = "操作人员") + private String operName; + + /** + * 部门名称 + */ + @Excel(name = "部门名称") + private String deptName; + + /** + * 请求url + */ + @Excel(name = "请求地址") + private String operUrl; + + /** + * 操作地址 + */ + @Excel(name = "操作地址") + private String operIp; + + /** + * 操作地点 + */ + @Excel(name = "操作地点") + private String operLocation; + + /** + * 请求参数 + */ + @Excel(name = "请求参数") + private String operParam; + + /** + * 返回参数 + */ + @Excel(name = "返回参数") + private String jsonResult; + + /** + * 操作状态(0正常 1异常) + */ + @Excel(name = "状态", readConverterExp = "0=正常,1=异常") + private Integer status; + + /** + * 错误消息 + */ + @Excel(name = "错误消息") + private String errorMsg; + + /** + * 操作时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "操作时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + private Date operTime; + + /** + * 请求参数 + */ + @TableField(exist = false) + private Map params = new HashMap<>(); + +} diff --git a/bashi-system/src/main/java/com/bashi/system/domain/SysPost.java b/bashi-system/src/main/java/com/bashi/system/domain/SysPost.java new file mode 100644 index 0000000..2db7908 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/domain/SysPost.java @@ -0,0 +1,110 @@ +package com.bashi.system.domain; + +import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.bashi.common.annotation.Excel; +import com.bashi.common.annotation.Excel.ColumnType; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * 岗位表 sys_post + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +@TableName("sys_post") +public class SysPost implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 岗位序号 + */ + @Excel(name = "岗位序号", cellType = ColumnType.NUMERIC) + @TableId(value = "post_id", type = IdType.AUTO) + private Long postId; + + /** + * 岗位编码 + */ + @Excel(name = "岗位编码") + @NotBlank(message = "岗位编码不能为空") + @Size(min = 0, max = 64, message = "岗位编码长度不能超过64个字符") + private String postCode; + + /** + * 岗位名称 + */ + @Excel(name = "岗位名称") + @NotBlank(message = "岗位名称不能为空") + @Size(min = 0, max = 50, message = "岗位名称长度不能超过50个字符") + private String postName; + + /** + * 岗位排序 + */ + @Excel(name = "岗位排序") + @NotBlank(message = "显示顺序不能为空") + private String postSort; + + /** + * 状态(0正常 1停用) + */ + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** + * 创建者 + */ + @TableField(fill = FieldFill.INSERT) + private String createBy; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 更新者 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateBy; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** + * 备注 + */ + private String remark; + + /** + * 请求参数 + */ + @TableField(exist = false) + private Map params = new HashMap<>(); + + /** + * 用户是否存在此岗位标识 默认不存在 + */ + @TableField(exist = false) + private boolean flag = false; + +} diff --git a/bashi-system/src/main/java/com/bashi/system/domain/SysRoleDept.java b/bashi-system/src/main/java/com/bashi/system/domain/SysRoleDept.java new file mode 100644 index 0000000..c391293 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/domain/SysRoleDept.java @@ -0,0 +1,29 @@ +package com.bashi.system.domain; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * 角色和部门关联 sys_role_dept + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +@TableName("sys_role_dept") +public class SysRoleDept { + /** + * 角色ID + */ + private Long roleId; + + /** + * 部门ID + */ + private Long deptId; + +} diff --git a/bashi-system/src/main/java/com/bashi/system/domain/SysRoleMenu.java b/bashi-system/src/main/java/com/bashi/system/domain/SysRoleMenu.java new file mode 100644 index 0000000..c845c0b --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/domain/SysRoleMenu.java @@ -0,0 +1,29 @@ +package com.bashi.system.domain; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * 角色和菜单关联 sys_role_menu + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +@TableName("sys_role_menu") +public class SysRoleMenu { + /** + * 角色ID + */ + private Long roleId; + + /** + * 菜单ID + */ + private Long menuId; + +} diff --git a/bashi-system/src/main/java/com/bashi/system/domain/SysUserOnline.java b/bashi-system/src/main/java/com/bashi/system/domain/SysUserOnline.java new file mode 100644 index 0000000..33b6ddf --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/domain/SysUserOnline.java @@ -0,0 +1,56 @@ +package com.bashi.system.domain; + +import lombok.*; +import lombok.experimental.Accessors; + +/** + * 当前在线会话 + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class SysUserOnline { + /** + * 会话编号 + */ + private String tokenId; + + /** + * 部门名称 + */ + private String deptName; + + /** + * 用户名称 + */ + private String userName; + + /** + * 登录IP地址 + */ + private String ipaddr; + + /** + * 登录地址 + */ + private String loginLocation; + + /** + * 浏览器类型 + */ + private String browser; + + /** + * 操作系统 + */ + private String os; + + /** + * 登录时间 + */ + private Long loginTime; + +} diff --git a/bashi-system/src/main/java/com/bashi/system/domain/SysUserPost.java b/bashi-system/src/main/java/com/bashi/system/domain/SysUserPost.java new file mode 100644 index 0000000..27ed827 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/domain/SysUserPost.java @@ -0,0 +1,29 @@ +package com.bashi.system.domain; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * 用户和岗位关联 sys_user_post + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +@TableName("sys_user_post") +public class SysUserPost { + /** + * 用户ID + */ + private Long userId; + + /** + * 岗位ID + */ + private Long postId; + +} diff --git a/bashi-system/src/main/java/com/bashi/system/domain/SysUserRole.java b/bashi-system/src/main/java/com/bashi/system/domain/SysUserRole.java new file mode 100644 index 0000000..92b3355 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/domain/SysUserRole.java @@ -0,0 +1,29 @@ +package com.bashi.system.domain; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * 用户和角色关联 sys_user_role + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +@TableName("sys_user_role") +public class SysUserRole { + /** + * 用户ID + */ + private Long userId; + + /** + * 角色ID + */ + private Long roleId; + +} diff --git a/bashi-system/src/main/java/com/bashi/system/domain/vo/MetaVo.java b/bashi-system/src/main/java/com/bashi/system/domain/vo/MetaVo.java new file mode 100644 index 0000000..a678fa0 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/domain/vo/MetaVo.java @@ -0,0 +1,42 @@ +package com.bashi.system.domain.vo; + +import lombok.*; +import lombok.experimental.Accessors; + +/** + * 路由显示信息 + * + * @author duteliang + */ + +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class MetaVo { + /** + * 设置该路由在侧边栏和面包屑中展示的名字 + */ + private String title; + + /** + * 设置该路由的图标,对应路径src/assets/icons/svg + */ + private String icon; + + /** + * 设置为true,则不会被 缓存 + */ + private boolean noCache; + + public MetaVo(String title, String icon) { + this.title = title; + this.icon = icon; + } + + public MetaVo(String title, String icon, boolean noCache) { + this.title = title; + this.icon = icon; + this.noCache = noCache; + } + +} diff --git a/bashi-system/src/main/java/com/bashi/system/domain/vo/RouterVo.java b/bashi-system/src/main/java/com/bashi/system/domain/vo/RouterVo.java new file mode 100644 index 0000000..682ff0c --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/domain/vo/RouterVo.java @@ -0,0 +1,59 @@ +package com.bashi.system.domain.vo; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.*; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * 路由配置信息 + * + * @author duteliang + */ +@Data +@NoArgsConstructor +@Accessors(chain = true) +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class RouterVo { + /** + * 路由名字 + */ + private String name; + + /** + * 路由地址 + */ + private String path; + + /** + * 是否隐藏路由,当设置 true 的时候该路由不会再侧边栏出现 + */ + private boolean hidden; + + /** + * 重定向地址,当设置 noRedirect 的时候该路由在面包屑导航中不可被点击 + */ + private String redirect; + + /** + * 组件地址 + */ + private String component; + + /** + * 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面 + */ + private Boolean alwaysShow; + + /** + * 其他元素 + */ + private MetaVo meta; + + /** + * 子路由 + */ + private List children; + +} diff --git a/bashi-system/src/main/java/com/bashi/system/mapper/SysConfigMapper.java b/bashi-system/src/main/java/com/bashi/system/mapper/SysConfigMapper.java new file mode 100644 index 0000000..9870f04 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/mapper/SysConfigMapper.java @@ -0,0 +1,13 @@ +package com.bashi.system.mapper; + +import com.bashi.common.core.mybatisplus.core.BaseMapperPlus; +import com.bashi.system.domain.SysConfig; + +/** + * 参数配置 数据层 + * + * @author duteliang + */ +public interface SysConfigMapper extends BaseMapperPlus { + +} diff --git a/bashi-system/src/main/java/com/bashi/system/mapper/SysDeptMapper.java b/bashi-system/src/main/java/com/bashi/system/mapper/SysDeptMapper.java new file mode 100644 index 0000000..ff5d61d --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/mapper/SysDeptMapper.java @@ -0,0 +1,41 @@ +package com.bashi.system.mapper; + +import com.bashi.common.core.domain.entity.SysDept; +import com.bashi.common.core.mybatisplus.core.BaseMapperPlus; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 部门管理 数据层 + * + * @author duteliang + */ +public interface SysDeptMapper extends BaseMapperPlus { + + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + public List selectDeptList(SysDept dept); + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @param deptCheckStrictly 部门树选择项是否关联显示 + * @return 选中部门列表 + */ + public List selectDeptListByRoleId(@Param("roleId") Long roleId, @Param("deptCheckStrictly") boolean deptCheckStrictly); + + /** + * 修改子元素关系 + * + * @param depts 子元素 + * @return 结果 + */ + public int updateDeptChildren(@Param("depts") List depts); + +} diff --git a/bashi-system/src/main/java/com/bashi/system/mapper/SysDictDataMapper.java b/bashi-system/src/main/java/com/bashi/system/mapper/SysDictDataMapper.java new file mode 100644 index 0000000..ccb78a0 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/mapper/SysDictDataMapper.java @@ -0,0 +1,23 @@ +package com.bashi.system.mapper; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.bashi.common.core.domain.entity.SysDictData; +import com.bashi.common.core.mybatisplus.core.BaseMapperPlus; + +import java.util.List; + +/** + * 字典表 数据层 + * + * @author duteliang + */ +public interface SysDictDataMapper extends BaseMapperPlus { + + default List selectDictDataByType(String dictType) { + return selectList( + new LambdaQueryWrapper() + .eq(SysDictData::getStatus, "0") + .eq(SysDictData::getDictType, dictType) + .orderByAsc(SysDictData::getDictSort)); + } +} diff --git a/bashi-system/src/main/java/com/bashi/system/mapper/SysDictTypeMapper.java b/bashi-system/src/main/java/com/bashi/system/mapper/SysDictTypeMapper.java new file mode 100644 index 0000000..94680ae --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/mapper/SysDictTypeMapper.java @@ -0,0 +1,13 @@ +package com.bashi.system.mapper; + +import com.bashi.common.core.domain.entity.SysDictType; +import com.bashi.common.core.mybatisplus.core.BaseMapperPlus; + +/** + * 字典表 数据层 + * + * @author duteliang + */ +public interface SysDictTypeMapper extends BaseMapperPlus { + +} diff --git a/bashi-system/src/main/java/com/bashi/system/mapper/SysLogininforMapper.java b/bashi-system/src/main/java/com/bashi/system/mapper/SysLogininforMapper.java new file mode 100644 index 0000000..d143035 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/mapper/SysLogininforMapper.java @@ -0,0 +1,13 @@ +package com.bashi.system.mapper; + +import com.bashi.common.core.mybatisplus.core.BaseMapperPlus; +import com.bashi.system.domain.SysLogininfor; + +/** + * 系统访问日志情况信息 数据层 + * + * @author duteliang + */ +public interface SysLogininforMapper extends BaseMapperPlus { + +} diff --git a/bashi-system/src/main/java/com/bashi/system/mapper/SysMenuMapper.java b/bashi-system/src/main/java/com/bashi/system/mapper/SysMenuMapper.java new file mode 100644 index 0000000..bed7790 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/mapper/SysMenuMapper.java @@ -0,0 +1,63 @@ +package com.bashi.system.mapper; + +import com.bashi.common.core.domain.entity.SysMenu; +import com.bashi.common.core.mybatisplus.core.BaseMapperPlus; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 菜单表 数据层 + * + * @author duteliang + */ +public interface SysMenuMapper extends BaseMapperPlus { + + /** + * 根据用户所有权限 + * + * @return 权限列表 + */ + public List selectMenuPerms(); + + /** + * 根据用户查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + public List selectMenuListByUserId(SysMenu menu); + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + public List selectMenuPermsByUserId(Long userId); + + /** + * 根据用户ID查询菜单 + * + * @return 菜单列表 + */ + public List selectMenuTreeAll(); + + /** + * 根据用户ID查询菜单 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuTreeByUserId(Long userId); + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @param menuCheckStrictly 菜单树选择项是否关联显示 + * @return 选中菜单列表 + */ + public List selectMenuListByRoleId(@Param("roleId") Long roleId, @Param("menuCheckStrictly") boolean menuCheckStrictly); + +} diff --git a/bashi-system/src/main/java/com/bashi/system/mapper/SysNoticeMapper.java b/bashi-system/src/main/java/com/bashi/system/mapper/SysNoticeMapper.java new file mode 100644 index 0000000..c546fbd --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/mapper/SysNoticeMapper.java @@ -0,0 +1,13 @@ +package com.bashi.system.mapper; + +import com.bashi.common.core.mybatisplus.core.BaseMapperPlus; +import com.bashi.system.domain.SysNotice; + +/** + * 通知公告表 数据层 + * + * @author duteliang + */ +public interface SysNoticeMapper extends BaseMapperPlus { + +} diff --git a/bashi-system/src/main/java/com/bashi/system/mapper/SysOperLogMapper.java b/bashi-system/src/main/java/com/bashi/system/mapper/SysOperLogMapper.java new file mode 100644 index 0000000..53488e8 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/mapper/SysOperLogMapper.java @@ -0,0 +1,13 @@ +package com.bashi.system.mapper; + +import com.bashi.common.core.mybatisplus.core.BaseMapperPlus; +import com.bashi.system.domain.SysOperLog; + +/** + * 操作日志 数据层 + * + * @author duteliang + */ +public interface SysOperLogMapper extends BaseMapperPlus { + +} diff --git a/bashi-system/src/main/java/com/bashi/system/mapper/SysPostMapper.java b/bashi-system/src/main/java/com/bashi/system/mapper/SysPostMapper.java new file mode 100644 index 0000000..d7e5aa9 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/mapper/SysPostMapper.java @@ -0,0 +1,31 @@ +package com.bashi.system.mapper; + +import com.bashi.common.core.mybatisplus.core.BaseMapperPlus; +import com.bashi.system.domain.SysPost; + +import java.util.List; + +/** + * 岗位信息 数据层 + * + * @author duteliang + */ +public interface SysPostMapper extends BaseMapperPlus { + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + public List selectPostListByUserId(Long userId); + + /** + * 查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + public List selectPostsByUserName(String userName); + +} diff --git a/bashi-system/src/main/java/com/bashi/system/mapper/SysRoleDeptMapper.java b/bashi-system/src/main/java/com/bashi/system/mapper/SysRoleDeptMapper.java new file mode 100644 index 0000000..4bba4d6 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/mapper/SysRoleDeptMapper.java @@ -0,0 +1,13 @@ +package com.bashi.system.mapper; + +import com.bashi.common.core.mybatisplus.core.BaseMapperPlus; +import com.bashi.system.domain.SysRoleDept; + +/** + * 角色与部门关联表 数据层 + * + * @author duteliang + */ +public interface SysRoleDeptMapper extends BaseMapperPlus { + +} diff --git a/bashi-system/src/main/java/com/bashi/system/mapper/SysRoleMapper.java b/bashi-system/src/main/java/com/bashi/system/mapper/SysRoleMapper.java new file mode 100644 index 0000000..5e2df34 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/mapper/SysRoleMapper.java @@ -0,0 +1,52 @@ +package com.bashi.system.mapper; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.bashi.common.core.domain.entity.SysRole; +import com.bashi.common.core.mybatisplus.core.BaseMapperPlus; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 角色表 数据层 + * + * @author duteliang + */ +public interface SysRoleMapper extends BaseMapperPlus { + + Page selectPageRoleList(@Param("page") Page page, @Param("role") SysRole role); + + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + public List selectRoleList(SysRole role); + + /** + * 根据用户ID查询角色 + * + * @param userId 用户ID + * @return 角色列表 + */ + public List selectRolePermissionByUserId(Long userId); + + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + public List selectRoleListByUserId(Long userId); + + /** + * 根据用户ID查询角色 + * + * @param userName 用户名 + * @return 角色列表 + */ + public List selectRolesByUserName(String userName); + +} diff --git a/bashi-system/src/main/java/com/bashi/system/mapper/SysRoleMenuMapper.java b/bashi-system/src/main/java/com/bashi/system/mapper/SysRoleMenuMapper.java new file mode 100644 index 0000000..3bb645f --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/mapper/SysRoleMenuMapper.java @@ -0,0 +1,13 @@ +package com.bashi.system.mapper; + +import com.bashi.common.core.mybatisplus.core.BaseMapperPlus; +import com.bashi.system.domain.SysRoleMenu; + +/** + * 角色与菜单关联表 数据层 + * + * @author duteliang + */ +public interface SysRoleMenuMapper extends BaseMapperPlus { + +} diff --git a/bashi-system/src/main/java/com/bashi/system/mapper/SysUserMapper.java b/bashi-system/src/main/java/com/bashi/system/mapper/SysUserMapper.java new file mode 100644 index 0000000..127183f --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/mapper/SysUserMapper.java @@ -0,0 +1,46 @@ +package com.bashi.system.mapper; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.bashi.common.core.domain.entity.SysUser; +import com.bashi.common.core.mybatisplus.core.BaseMapperPlus; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 用户表 数据层 + * + * @author duteliang + */ +public interface SysUserMapper extends BaseMapperPlus { + + Page selectPageUserList(@Param("page") Page page, @Param("user") SysUser user); + + /** + * 根据条件分页查询用户列表 + * + * @param sysUser 用户信息 + * @return 用户信息集合信息 + */ + public List selectUserList(SysUser sysUser); + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + public SysUser selectUserByUserName(String userName); + + public SysUser selectUserByMobile(String mobile); + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + public SysUser selectUserById(Long userId); + + SysUser selectUserByOpenId(String openId); +} diff --git a/bashi-system/src/main/java/com/bashi/system/mapper/SysUserPostMapper.java b/bashi-system/src/main/java/com/bashi/system/mapper/SysUserPostMapper.java new file mode 100644 index 0000000..22be36b --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/mapper/SysUserPostMapper.java @@ -0,0 +1,13 @@ +package com.bashi.system.mapper; + +import com.bashi.common.core.mybatisplus.core.BaseMapperPlus; +import com.bashi.system.domain.SysUserPost; + +/** + * 用户与岗位关联表 数据层 + * + * @author duteliang + */ +public interface SysUserPostMapper extends BaseMapperPlus { + +} diff --git a/bashi-system/src/main/java/com/bashi/system/mapper/SysUserRoleMapper.java b/bashi-system/src/main/java/com/bashi/system/mapper/SysUserRoleMapper.java new file mode 100644 index 0000000..24c7f99 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/mapper/SysUserRoleMapper.java @@ -0,0 +1,13 @@ +package com.bashi.system.mapper; + +import com.bashi.common.core.mybatisplus.core.BaseMapperPlus; +import com.bashi.system.domain.SysUserRole; + +/** + * 用户与角色关联表 数据层 + * + * @author duteliang + */ +public interface SysUserRoleMapper extends BaseMapperPlus { + +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/ISysConfigService.java b/bashi-system/src/main/java/com/bashi/system/service/ISysConfigService.java new file mode 100644 index 0000000..7e0b33b --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/ISysConfigService.java @@ -0,0 +1,89 @@ +package com.bashi.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.system.domain.SysConfig; + +import java.util.List; + +/** + * 参数配置 服务层 + * + * @author duteliang + */ +public interface ISysConfigService extends IService { + + + TableDataInfo selectPageConfigList(SysConfig config); + + /** + * 查询参数配置信息 + * + * @param configId 参数配置ID + * @return 参数配置信息 + */ + public SysConfig selectConfigById(Long configId); + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数键名 + * @return 参数键值 + */ + public String selectConfigByKey(String configKey); + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + public List selectConfigList(SysConfig config); + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int insertConfig(SysConfig config); + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + public int updateConfig(SysConfig config); + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + * @return 结果 + */ + public void deleteConfigByIds(Long[] configIds); + + /** + * 加载参数缓存数据 + */ + public void loadingConfigCache(); + + /** + * 清空参数缓存数据 + */ + public void clearConfigCache(); + + /** + * 重置参数缓存数据 + */ + public void resetConfigCache(); + + /** + * 校验参数键名是否唯一 + * + * @param config 参数信息 + * @return 结果 + */ + public String checkConfigKeyUnique(SysConfig config); +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/ISysDeptService.java b/bashi-system/src/main/java/com/bashi/system/service/ISysDeptService.java new file mode 100644 index 0000000..eed8b02 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/ISysDeptService.java @@ -0,0 +1,110 @@ +package com.bashi.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bashi.common.core.domain.TreeSelect; +import com.bashi.common.core.domain.entity.SysDept; + +import java.util.List; + +/** + * 部门管理 服务层 + * + * @author duteliang + */ +public interface ISysDeptService extends IService { + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + public List selectDeptList(SysDept dept); + + /** + * 构建前端所需要树结构 + * + * @param depts 部门列表 + * @return 树结构列表 + */ + public List buildDeptTree(List depts); + + /** + * 构建前端所需要下拉树结构 + * + * @param depts 部门列表 + * @return 下拉树结构列表 + */ + public List buildDeptTreeSelect(List depts); + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @return 选中部门列表 + */ + public List selectDeptListByRoleId(Long roleId); + + /** + * 根据部门ID查询信息 + * + * @param deptId 部门ID + * @return 部门信息 + */ + public SysDept selectDeptById(Long deptId); + + /** + * 根据ID查询所有子部门(正常状态) + * + * @param deptId 部门ID + * @return 子部门数 + */ + public long selectNormalChildrenDeptById(Long deptId); + + /** + * 是否存在部门子节点 + * + * @param deptId 部门ID + * @return 结果 + */ + public boolean hasChildByDeptId(Long deptId); + + /** + * 查询部门是否存在用户 + * + * @param deptId 部门ID + * @return 结果 true 存在 false 不存在 + */ + public boolean checkDeptExistUser(Long deptId); + + /** + * 校验部门名称是否唯一 + * + * @param dept 部门信息 + * @return 结果 + */ + public String checkDeptNameUnique(SysDept dept); + + /** + * 新增保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int insertDept(SysDept dept); + + /** + * 修改保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + public int updateDept(SysDept dept); + + /** + * 删除部门管理信息 + * + * @param deptId 部门ID + * @return 结果 + */ + public int deleteDeptById(Long deptId); +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/ISysDictDataService.java b/bashi-system/src/main/java/com/bashi/system/service/ISysDictDataService.java new file mode 100644 index 0000000..131cffc --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/ISysDictDataService.java @@ -0,0 +1,67 @@ +package com.bashi.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bashi.common.core.domain.entity.SysDictData; +import com.bashi.common.core.page.TableDataInfo; + +import java.util.List; + +/** + * 字典 业务层 + * + * @author duteliang + */ +public interface ISysDictDataService extends IService { + + + TableDataInfo selectPageDictDataList(SysDictData dictData); + + /** + * 根据条件分页查询字典数据 + * + * @param dictData 字典数据信息 + * @return 字典数据集合信息 + */ + public List selectDictDataList(SysDictData dictData); + + /** + * 根据字典类型和字典键值查询字典数据信息 + * + * @param dictType 字典类型 + * @param dictValue 字典键值 + * @return 字典标签 + */ + public String selectDictLabel(String dictType, String dictValue); + + /** + * 根据字典数据ID查询信息 + * + * @param dictCode 字典数据ID + * @return 字典数据 + */ + public SysDictData selectDictDataById(Long dictCode); + + /** + * 批量删除字典数据信息 + * + * @param dictCodes 需要删除的字典数据ID + * @return 结果 + */ + public void deleteDictDataByIds(Long[] dictCodes); + + /** + * 新增保存字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int insertDictData(SysDictData dictData); + + /** + * 修改保存字典数据信息 + * + * @param dictData 字典数据信息 + * @return 结果 + */ + public int updateDictData(SysDictData dictData); +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/ISysDictTypeService.java b/bashi-system/src/main/java/com/bashi/system/service/ISysDictTypeService.java new file mode 100644 index 0000000..b519984 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/ISysDictTypeService.java @@ -0,0 +1,105 @@ +package com.bashi.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bashi.common.core.domain.entity.SysDictData; +import com.bashi.common.core.domain.entity.SysDictType; +import com.bashi.common.core.page.TableDataInfo; + +import java.util.List; + +/** + * 字典 业务层 + * + * @author duteliang + */ +public interface ISysDictTypeService extends IService { + + + TableDataInfo selectPageDictTypeList(SysDictType dictType); + + /** + * 根据条件分页查询字典类型 + * + * @param dictType 字典类型信息 + * @return 字典类型集合信息 + */ + public List selectDictTypeList(SysDictType dictType); + + /** + * 根据所有字典类型 + * + * @return 字典类型集合信息 + */ + public List selectDictTypeAll(); + + /** + * 根据字典类型查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据集合信息 + */ + public List selectDictDataByType(String dictType); + + /** + * 根据字典类型ID查询信息 + * + * @param dictId 字典类型ID + * @return 字典类型 + */ + public SysDictType selectDictTypeById(Long dictId); + + /** + * 根据字典类型查询信息 + * + * @param dictType 字典类型 + * @return 字典类型 + */ + public SysDictType selectDictTypeByType(String dictType); + + /** + * 批量删除字典信息 + * + * @param dictIds 需要删除的字典ID + * @return 结果 + */ + public void deleteDictTypeByIds(Long[] dictIds); + + /** + * 加载字典缓存数据 + */ + public void loadingDictCache(); + + /** + * 清空字典缓存数据 + */ + public void clearDictCache(); + + /** + * 重置字典缓存数据 + */ + public void resetDictCache(); + + /** + * 新增保存字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int insertDictType(SysDictType dictType); + + /** + * 修改保存字典类型信息 + * + * @param dictType 字典类型信息 + * @return 结果 + */ + public int updateDictType(SysDictType dictType); + + /** + * 校验字典类型称是否唯一 + * + * @param dictType 字典类型 + * @return 结果 + */ + public String checkDictTypeUnique(SysDictType dictType); +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/ISysLogininforService.java b/bashi-system/src/main/java/com/bashi/system/service/ISysLogininforService.java new file mode 100644 index 0000000..cd84ca5 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/ISysLogininforService.java @@ -0,0 +1,46 @@ +package com.bashi.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.system.domain.SysLogininfor; + +import java.util.List; + +/** + * 系统访问日志情况信息 服务层 + * + * @author duteliang + */ +public interface ISysLogininforService extends IService { + + + TableDataInfo selectPageLogininforList(SysLogininfor logininfor); + + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + public void insertLogininfor(SysLogininfor logininfor); + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + public List selectLogininforList(SysLogininfor logininfor); + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + * @return + */ + public int deleteLogininforByIds(Long[] infoIds); + + /** + * 清空系统登录日志 + */ + public void cleanLogininfor(); +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/ISysMenuService.java b/bashi-system/src/main/java/com/bashi/system/service/ISysMenuService.java new file mode 100644 index 0000000..c919a85 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/ISysMenuService.java @@ -0,0 +1,137 @@ +package com.bashi.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bashi.common.core.domain.TreeSelect; +import com.bashi.common.core.domain.entity.SysMenu; +import com.bashi.system.domain.vo.RouterVo; + +import java.util.List; +import java.util.Set; + +/** + * 菜单 业务层 + * + * @author duteliang + */ +public interface ISysMenuService extends IService { + /** + * 根据用户查询系统菜单列表 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuList(Long userId); + + /** + * 根据用户查询系统菜单列表 + * + * @param menu 菜单信息 + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuList(SysMenu menu, Long userId); + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + public Set selectMenuPermsByUserId(Long userId); + + /** + * 根据用户ID查询菜单树信息 + * + * @param userId 用户ID + * @return 菜单列表 + */ + public List selectMenuTreeByUserId(Long userId); + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @return 选中菜单列表 + */ + public List selectMenuListByRoleId(Long roleId); + + /** + * 构建前端路由所需要的菜单 + * + * @param menus 菜单列表 + * @return 路由列表 + */ + public List buildMenus(List menus); + + /** + * 构建前端所需要树结构 + * + * @param menus 菜单列表 + * @return 树结构列表 + */ + public List buildMenuTree(List menus); + + /** + * 构建前端所需要下拉树结构 + * + * @param menus 菜单列表 + * @return 下拉树结构列表 + */ + public List buildMenuTreeSelect(List menus); + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + public SysMenu selectMenuById(Long menuId); + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 true 存在 false 不存在 + */ + public boolean hasChildByMenuId(Long menuId); + + /** + * 查询菜单是否存在角色 + * + * @param menuId 菜单ID + * @return 结果 true 存在 false 不存在 + */ + public boolean checkMenuExistRole(Long menuId); + + /** + * 新增保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int insertMenu(SysMenu menu); + + /** + * 修改保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + public int updateMenu(SysMenu menu); + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + public int deleteMenuById(Long menuId); + + /** + * 校验菜单名称是否唯一 + * + * @param menu 菜单信息 + * @return 结果 + */ + public String checkMenuNameUnique(SysMenu menu); +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/ISysNoticeService.java b/bashi-system/src/main/java/com/bashi/system/service/ISysNoticeService.java new file mode 100644 index 0000000..11647ce --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/ISysNoticeService.java @@ -0,0 +1,66 @@ +package com.bashi.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.system.domain.SysNotice; + +import java.util.List; + +/** + * 公告 服务层 + * + * @author duteliang + */ +public interface ISysNoticeService extends IService { + + + TableDataInfo selectPageNoticeList(SysNotice notice); + + /** + * 查询公告信息 + * + * @param noticeId 公告ID + * @return 公告信息 + */ + public SysNotice selectNoticeById(Long noticeId); + + /** + * 查询公告列表 + * + * @param notice 公告信息 + * @return 公告集合 + */ + public List selectNoticeList(SysNotice notice); + + /** + * 新增公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int insertNotice(SysNotice notice); + + /** + * 修改公告 + * + * @param notice 公告信息 + * @return 结果 + */ + public int updateNotice(SysNotice notice); + + /** + * 删除公告信息 + * + * @param noticeId 公告ID + * @return 结果 + */ + public int deleteNoticeById(Long noticeId); + + /** + * 批量删除公告信息 + * + * @param noticeIds 需要删除的公告ID + * @return 结果 + */ + public int deleteNoticeByIds(Long[] noticeIds); +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/ISysOperLogService.java b/bashi-system/src/main/java/com/bashi/system/service/ISysOperLogService.java new file mode 100644 index 0000000..cfe858d --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/ISysOperLogService.java @@ -0,0 +1,53 @@ +package com.bashi.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.system.domain.SysOperLog; + +import java.util.List; + +/** + * 操作日志 服务层 + * + * @author duteliang + */ +public interface ISysOperLogService extends IService { + + TableDataInfo selectPageOperLogList(SysOperLog operLog); + + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + */ + public void insertOperlog(SysOperLog operLog); + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + public List selectOperLogList(SysOperLog operLog); + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + public int deleteOperLogByIds(Long[] operIds); + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + public SysOperLog selectOperLogById(Long operId); + + /** + * 清空操作日志 + */ + public void cleanOperLog(); +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/ISysPostService.java b/bashi-system/src/main/java/com/bashi/system/service/ISysPostService.java new file mode 100644 index 0000000..10c5355 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/ISysPostService.java @@ -0,0 +1,106 @@ +package com.bashi.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.system.domain.SysPost; + +import java.util.List; + +/** + * 岗位信息 服务层 + * + * @author duteliang + */ +public interface ISysPostService extends IService { + + + TableDataInfo selectPagePostList(SysPost post); + + /** + * 查询岗位信息集合 + * + * @param post 岗位信息 + * @return 岗位列表 + */ + public List selectPostList(SysPost post); + + /** + * 查询所有岗位 + * + * @return 岗位列表 + */ + public List selectPostAll(); + + /** + * 通过岗位ID查询岗位信息 + * + * @param postId 岗位ID + * @return 角色对象信息 + */ + public SysPost selectPostById(Long postId); + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + public List selectPostListByUserId(Long userId); + + /** + * 校验岗位名称 + * + * @param post 岗位信息 + * @return 结果 + */ + public String checkPostNameUnique(SysPost post); + + /** + * 校验岗位编码 + * + * @param post 岗位信息 + * @return 结果 + */ + public String checkPostCodeUnique(SysPost post); + + /** + * 通过岗位ID查询岗位使用数量 + * + * @param postId 岗位ID + * @return 结果 + */ + public long countUserPostById(Long postId); + + /** + * 删除岗位信息 + * + * @param postId 岗位ID + * @return 结果 + */ + public int deletePostById(Long postId); + + /** + * 批量删除岗位信息 + * + * @param postIds 需要删除的岗位ID + * @return 结果 + * @throws Exception 异常 + */ + public int deletePostByIds(Long[] postIds); + + /** + * 新增保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int insertPost(SysPost post); + + /** + * 修改保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + public int updatePost(SysPost post); +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/ISysRoleService.java b/bashi-system/src/main/java/com/bashi/system/service/ISysRoleService.java new file mode 100644 index 0000000..2604943 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/ISysRoleService.java @@ -0,0 +1,137 @@ +package com.bashi.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bashi.common.core.domain.entity.SysRole; +import com.bashi.common.core.page.TableDataInfo; + +import java.util.List; +import java.util.Set; + +/** + * 角色业务层 + * + * @author duteliang + */ +public interface ISysRoleService extends IService { + + + TableDataInfo selectPageRoleList(SysRole role); + + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + public List selectRoleList(SysRole role); + + /** + * 根据用户ID查询角色 + * + * @param userId 用户ID + * @return 权限列表 + */ + public Set selectRolePermissionByUserId(Long userId); + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + public List selectRoleAll(); + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + public List selectRoleListByUserId(Long userId); + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + public SysRole selectRoleById(Long roleId); + + /** + * 校验角色名称是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + public String checkRoleNameUnique(SysRole role); + + /** + * 校验角色权限是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + public String checkRoleKeyUnique(SysRole role); + + /** + * 校验角色是否允许操作 + * + * @param role 角色信息 + */ + public void checkRoleAllowed(SysRole role); + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + public long countUserRoleByRoleId(Long roleId); + + /** + * 新增保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int insertRole(SysRole role); + + /** + * 修改保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRole(SysRole role); + + /** + * 修改角色状态 + * + * @param role 角色信息 + * @return 结果 + */ + public int updateRoleStatus(SysRole role); + + /** + * 修改数据权限信息 + * + * @param role 角色信息 + * @return 结果 + */ + public int authDataScope(SysRole role); + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + public int deleteRoleById(Long roleId); + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + public int deleteRoleByIds(Long[] roleIds); +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/ISysUserOnlineService.java b/bashi-system/src/main/java/com/bashi/system/service/ISysUserOnlineService.java new file mode 100644 index 0000000..7369a53 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/ISysUserOnlineService.java @@ -0,0 +1,47 @@ +package com.bashi.system.service; + +import com.bashi.common.core.domain.model.LoginUser; +import com.bashi.system.domain.SysUserOnline; + +/** + * 在线用户 服务层 + * + * @author duteliang + */ +public interface ISysUserOnlineService { + /** + * 通过登录地址查询信息 + * + * @param ipaddr 登录地址 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByIpaddr(String ipaddr, LoginUser user); + + /** + * 通过用户名称查询信息 + * + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByUserName(String userName, LoginUser user); + + /** + * 通过登录地址/用户名称查询信息 + * + * @param ipaddr 登录地址 + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + public SysUserOnline selectOnlineByInfo(String ipaddr, String userName, LoginUser user); + + /** + * 设置在线用户信息 + * + * @param user 用户信息 + * @return 在线用户 + */ + public SysUserOnline loginUserToUserOnline(LoginUser user); +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/ISysUserService.java b/bashi-system/src/main/java/com/bashi/system/service/ISysUserService.java new file mode 100644 index 0000000..6d6623e --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/ISysUserService.java @@ -0,0 +1,178 @@ +package com.bashi.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bashi.common.core.domain.entity.SysUser; +import com.bashi.common.core.page.TableDataInfo; + +import java.util.List; + +/** + * 用户 业务层 + * + * @author duteliang + */ +public interface ISysUserService extends IService { + + + TableDataInfo selectPageUserList(SysUser user); + + /** + * 根据条件分页查询用户列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + public List selectUserList(SysUser user); + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + public SysUser selectUserByUserName(String userName); + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + public SysUser selectUserById(Long userId); + + /** + * 根据用户ID查询用户所属角色组 + * + * @param userName 用户名 + * @return 结果 + */ + public String selectUserRoleGroup(String userName); + + /** + * 根据用户ID查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + public String selectUserPostGroup(String userName); + + /** + * 校验用户名称是否唯一 + * + * @param userName 用户名称 + * @return 结果 + */ + public String checkUserNameUnique(String userName); + + /** + * 校验手机号码是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + public String checkPhoneUnique(SysUser user); + + /** + * 校验email是否唯一 + * + * @param user 用户信息 + * @return 结果 + */ + public String checkEmailUnique(SysUser user); + + /** + * 校验用户是否允许操作 + * + * @param user 用户信息 + */ + public void checkUserAllowed(SysUser user); + + /** + * 新增用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int insertUser(SysUser user); + + /** + * 修改用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUser(SysUser user); + + /** + * 修改用户状态 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUserStatus(SysUser user); + + /** + * 修改用户基本信息 + * + * @param user 用户信息 + * @return 结果 + */ + public int updateUserProfile(SysUser user); + + /** + * 修改用户头像 + * + * @param userName 用户名 + * @param avatar 头像地址 + * @return 结果 + */ + boolean updateUserAvatar(String userName, String avatar); + + /** + * 重置用户密码 + * + * @param user 用户信息 + * @return 结果 + */ + int resetPwd(SysUser user); + + /** + * 重置用户密码 + * + * @param userName 用户名 + * @param password 密码 + * @return 结果 + */ + int resetUserPwd(String userName, String password); + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + int deleteUserById(Long userId); + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + int deleteUserByIds(Long[] userIds); + + /** + * 导入用户数据 + * + * @param userList 用户数据列表 + * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据 + * @param operName 操作用户 + * @return 结果 + */ + String importUser(List userList, Boolean isUpdateSupport, String operName); + + SysUser selectUserByMobile(String mobile); + + SysUser selectUserByOpenId(String openId); + +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/impl/SysConfigServiceImpl.java b/bashi-system/src/main/java/com/bashi/system/service/impl/SysConfigServiceImpl.java new file mode 100644 index 0000000..5e4de45 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/impl/SysConfigServiceImpl.java @@ -0,0 +1,220 @@ +package com.bashi.system.service.impl; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bashi.common.annotation.DataSource; +import com.bashi.common.constant.Constants; +import com.bashi.common.constant.UserConstants; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.core.redis.RedisCache; +import com.bashi.common.enums.DataSourceType; +import com.bashi.common.exception.CustomException; +import com.bashi.common.utils.PageUtils; +import com.bashi.system.domain.SysConfig; +import com.bashi.system.mapper.SysConfigMapper; +import com.bashi.system.service.ISysConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 参数配置 服务层实现 + * + * @author duteliang + */ +@Service +public class SysConfigServiceImpl extends ServiceImpl implements ISysConfigService { + + @Autowired + private RedisCache redisCache; + + /** + * 项目启动时,初始化参数到缓存 + */ + @PostConstruct + public void init() { + loadingConfigCache(); + } + + @Override + public TableDataInfo selectPageConfigList(SysConfig config) { + Map params = config.getParams(); + LambdaQueryWrapper lqw = new LambdaQueryWrapper() + .like(StrUtil.isNotBlank(config.getConfigName()), SysConfig::getConfigName, config.getConfigName()) + .eq(StrUtil.isNotBlank(config.getConfigType()), SysConfig::getConfigType, config.getConfigType()) + .like(StrUtil.isNotBlank(config.getConfigKey()), SysConfig::getConfigKey, config.getConfigKey()) + .apply(Validator.isNotEmpty(params.get("beginTime")), + "date_format(create_time,'%y%m%d') >= date_format({0},'%y%m%d')", + params.get("beginTime")) + .apply(Validator.isNotEmpty(params.get("endTime")), + "date_format(create_time,'%y%m%d') <= date_format({0},'%y%m%d')", + params.get("endTime")); + return PageUtils.buildDataInfo(page(PageUtils.buildPage(), lqw)); + } + + /** + * 查询参数配置信息 + * + * @param configId 参数配置ID + * @return 参数配置信息 + */ + @Override + @DataSource(DataSourceType.MASTER) + public SysConfig selectConfigById(Long configId) { + return baseMapper.selectById(configId); + } + + /** + * 根据键名查询参数配置信息 + * + * @param configKey 参数key + * @return 参数键值 + */ + @Override + public String selectConfigByKey(String configKey) { + String configValue = Convert.toStr(redisCache.getCacheObject(getCacheKey(configKey))); + if (Validator.isNotEmpty(configValue)) { + return configValue; + } + SysConfig retConfig = baseMapper.selectOne(new LambdaQueryWrapper() + .eq(SysConfig::getConfigKey, configKey)); + if (Validator.isNotNull(retConfig)) { + redisCache.setCacheObject(getCacheKey(configKey), retConfig.getConfigValue()); + return retConfig.getConfigValue(); + } + return StrUtil.EMPTY; + } + + /** + * 查询参数配置列表 + * + * @param config 参数配置信息 + * @return 参数配置集合 + */ + @Override + public List selectConfigList(SysConfig config) { + Map params = config.getParams(); + LambdaQueryWrapper lqw = new LambdaQueryWrapper() + .like(StrUtil.isNotBlank(config.getConfigName()), SysConfig::getConfigName, config.getConfigName()) + .eq(StrUtil.isNotBlank(config.getConfigType()), SysConfig::getConfigType, config.getConfigType()) + .like(StrUtil.isNotBlank(config.getConfigKey()), SysConfig::getConfigKey, config.getConfigKey()) + .apply(Validator.isNotEmpty(params.get("beginTime")), + "date_format(create_time,'%y%m%d') >= date_format({0},'%y%m%d')", + params.get("beginTime")) + .apply(Validator.isNotEmpty(params.get("endTime")), + "date_format(create_time,'%y%m%d') <= date_format({0},'%y%m%d')", + params.get("endTime")); + return baseMapper.selectList(lqw); + } + + /** + * 新增参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public int insertConfig(SysConfig config) { + int row = baseMapper.insert(config); + if (row > 0) { + redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + return row; + } + + /** + * 修改参数配置 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public int updateConfig(SysConfig config) { + int row = baseMapper.updateById(config); + if (row > 0) { + redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + return row; + } + + /** + * 批量删除参数信息 + * + * @param configIds 需要删除的参数ID + * @return 结果 + */ + @Override + public void deleteConfigByIds(Long[] configIds) { + for (Long configId : configIds) { + SysConfig config = selectConfigById(configId); + if (StrUtil.equals(UserConstants.YES, config.getConfigType())) { + throw new CustomException(String.format("内置参数【%1$s】不能删除 ", config.getConfigKey())); + } + redisCache.deleteObject(getCacheKey(config.getConfigKey())); + } + baseMapper.deleteBatchIds(Arrays.asList(configIds)); + } + + /** + * 加载参数缓存数据 + */ + @Override + public void loadingConfigCache() { + List configsList = selectConfigList(new SysConfig()); + for (SysConfig config : configsList) { + redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue()); + } + } + + /** + * 清空参数缓存数据 + */ + @Override + public void clearConfigCache() { + Collection keys = redisCache.keys(Constants.SYS_CONFIG_KEY + "*"); + redisCache.deleteObject(keys); + } + + /** + * 重置参数缓存数据 + */ + @Override + public void resetConfigCache() { + clearConfigCache(); + loadingConfigCache(); + } + + /** + * 校验参数键名是否唯一 + * + * @param config 参数配置信息 + * @return 结果 + */ + @Override + public String checkConfigKeyUnique(SysConfig config) { + Long configId = Validator.isNull(config.getConfigId()) ? -1L : config.getConfigId(); + SysConfig info = baseMapper.selectOne(new LambdaQueryWrapper().eq(SysConfig::getConfigKey, config.getConfigKey())); + if (Validator.isNotNull(info) && info.getConfigId().longValue() != configId.longValue()) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 设置cache key + * + * @param configKey 参数键 + * @return 缓存键key + */ + private String getCacheKey(String configKey) { + return Constants.SYS_CONFIG_KEY + configKey; + } +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/impl/SysDeptServiceImpl.java b/bashi-system/src/main/java/com/bashi/system/service/impl/SysDeptServiceImpl.java new file mode 100644 index 0000000..d20ba5e --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/impl/SysDeptServiceImpl.java @@ -0,0 +1,289 @@ +package com.bashi.system.service.impl; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Validator; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bashi.common.annotation.DataScope; +import com.bashi.common.constant.UserConstants; +import com.bashi.common.core.domain.TreeSelect; +import com.bashi.common.core.domain.entity.SysDept; +import com.bashi.common.core.domain.entity.SysRole; +import com.bashi.common.core.domain.entity.SysUser; +import com.bashi.common.exception.CustomException; +import com.bashi.system.mapper.SysDeptMapper; +import com.bashi.system.mapper.SysRoleMapper; +import com.bashi.system.mapper.SysUserMapper; +import com.bashi.system.service.ISysDeptService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 部门管理 服务实现 + * + * @author duteliang + */ +@Service +public class SysDeptServiceImpl extends ServiceImpl implements ISysDeptService { + + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysUserMapper userMapper; + + /** + * 查询部门管理数据 + * + * @param dept 部门信息 + * @return 部门信息集合 + */ + @Override + @DataScope(deptAlias = "d") + public List selectDeptList(SysDept dept) { + return baseMapper.selectDeptList(dept); + } + + /** + * 构建前端所需要树结构 + * + * @param depts 部门列表 + * @return 树结构列表 + */ + @Override + public List buildDeptTree(List depts) { + List returnList = new ArrayList(); + List tempList = new ArrayList(); + for (SysDept dept : depts) { + tempList.add(dept.getDeptId()); + } + for (SysDept dept : depts) { + // 如果是顶级节点, 遍历该父节点的所有子节点 + if (!tempList.contains(dept.getParentId())) { + recursionFn(depts, dept); + returnList.add(dept); + } + } + if (returnList.isEmpty()) { + returnList = depts; + } + return returnList; + } + + /** + * 构建前端所需要下拉树结构 + * + * @param depts 部门列表 + * @return 下拉树结构列表 + */ + @Override + public List buildDeptTreeSelect(List depts) { + List deptTrees = buildDeptTree(depts); + return deptTrees.stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + /** + * 根据角色ID查询部门树信息 + * + * @param roleId 角色ID + * @return 选中部门列表 + */ + @Override + public List selectDeptListByRoleId(Long roleId) { + SysRole role = roleMapper.selectById(roleId); + return baseMapper.selectDeptListByRoleId(roleId, role.isDeptCheckStrictly()); + } + + /** + * 根据部门ID查询信息 + * + * @param deptId 部门ID + * @return 部门信息 + */ + @Override + public SysDept selectDeptById(Long deptId) { + return getById(deptId); + } + + /** + * 根据ID查询所有子部门(正常状态) + * + * @param deptId 部门ID + * @return 子部门数 + */ + @Override + public long selectNormalChildrenDeptById(Long deptId) { + return count(new LambdaQueryWrapper() + .eq(SysDept::getStatus, 0) + .apply("find_in_set({0}, ancestors)", deptId)); + } + + /** + * 是否存在子节点 + * + * @param deptId 部门ID + * @return 结果 + */ + @Override + public boolean hasChildByDeptId(Long deptId) { + long result = count(new LambdaQueryWrapper() + .eq(SysDept::getParentId, deptId) + .last("limit 1")); + return result > 0; + } + + /** + * 查询部门是否存在用户 + * + * @param deptId 部门ID + * @return 结果 true 存在 false 不存在 + */ + @Override + public boolean checkDeptExistUser(Long deptId) { + long result = userMapper.selectCount(new LambdaQueryWrapper() + .eq(SysUser::getDeptId, deptId)); + return result > 0; + } + + /** + * 校验部门名称是否唯一 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public String checkDeptNameUnique(SysDept dept) { + Long deptId = Validator.isNull(dept.getDeptId()) ? -1L : dept.getDeptId(); + SysDept info = getOne(new LambdaQueryWrapper() + .eq(SysDept::getDeptName, dept.getDeptName()) + .eq(SysDept::getParentId, dept.getParentId()) + .last("limit 1")); + if (Validator.isNotNull(info) && info.getDeptId().longValue() != deptId.longValue()) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 新增保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public int insertDept(SysDept dept) { + SysDept info = getById(dept.getParentId()); + // 如果父节点不为正常状态,则不允许新增子节点 + if (!UserConstants.DEPT_NORMAL.equals(info.getStatus())) { + throw new CustomException("部门停用,不允许新增"); + } + dept.setAncestors(info.getAncestors() + "," + dept.getParentId()); + return baseMapper.insert(dept); + } + + /** + * 修改保存部门信息 + * + * @param dept 部门信息 + * @return 结果 + */ + @Override + public int updateDept(SysDept dept) { + SysDept newParentDept = getById(dept.getParentId()); + SysDept oldDept = getById(dept.getDeptId()); + if (Validator.isNotNull(newParentDept) && Validator.isNotNull(oldDept)) { + String newAncestors = newParentDept.getAncestors() + "," + newParentDept.getDeptId(); + String oldAncestors = oldDept.getAncestors(); + dept.setAncestors(newAncestors); + updateDeptChildren(dept.getDeptId(), newAncestors, oldAncestors); + } + int result = baseMapper.updateById(dept); + if (UserConstants.DEPT_NORMAL.equals(dept.getStatus())) { + // 如果该部门是启用状态,则启用该部门的所有上级部门 + updateParentDeptStatusNormal(dept); + } + return result; + } + + /** + * 修改该部门的父级部门状态 + * + * @param dept 当前部门 + */ + private void updateParentDeptStatusNormal(SysDept dept) { + String ancestors = dept.getAncestors(); + Long[] deptIds = Convert.toLongArray(ancestors); + update(null, new LambdaUpdateWrapper() + .set(SysDept::getStatus, "0") + .in(SysDept::getDeptId, Arrays.asList(deptIds))); + } + + /** + * 修改子元素关系 + * + * @param deptId 被修改的部门ID + * @param newAncestors 新的父ID集合 + * @param oldAncestors 旧的父ID集合 + */ + public void updateDeptChildren(Long deptId, String newAncestors, String oldAncestors) { + List children = list(new LambdaQueryWrapper() + .apply("find_in_set({0},ancestors)",deptId)); + for (SysDept child : children) { + child.setAncestors(child.getAncestors().replaceFirst(oldAncestors, newAncestors)); + } + if (children.size() > 0) { + baseMapper.updateDeptChildren(children); + } + } + + /** + * 删除部门管理信息 + * + * @param deptId 部门ID + * @return 结果 + */ + @Override + public int deleteDeptById(Long deptId) { + return baseMapper.deleteById(deptId); + } + + /** + * 递归列表 + */ + private void recursionFn(List list, SysDept t) { + // 得到子节点列表 + List childList = getChildList(list, t); + t.setChildren(childList); + for (SysDept tChild : childList) { + if (hasChild(list, tChild)) { + recursionFn(list, tChild); + } + } + } + + /** + * 得到子节点列表 + */ + private List getChildList(List list, SysDept t) { + List tlist = new ArrayList(); + for (SysDept n : list) { + if (Validator.isNotNull(n.getParentId()) && n.getParentId().longValue() == t.getDeptId().longValue()) { + tlist.add(n); + } + } + return tlist; + } + + /** + * 判断是否有子节点 + */ + private boolean hasChild(List list, SysDept t) { + return getChildList(list, t).size() > 0; + } +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/impl/SysDictDataServiceImpl.java b/bashi-system/src/main/java/com/bashi/system/service/impl/SysDictDataServiceImpl.java new file mode 100644 index 0000000..0fe1369 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/impl/SysDictDataServiceImpl.java @@ -0,0 +1,124 @@ +package com.bashi.system.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bashi.common.core.domain.entity.SysDictData; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.utils.DictUtils; +import com.bashi.common.utils.PageUtils; +import com.bashi.system.mapper.SysDictDataMapper; +import com.bashi.system.service.ISysDictDataService; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.List; + +/** + * 字典 业务层处理 + * + * @author duteliang + */ +@Service +public class SysDictDataServiceImpl extends ServiceImpl implements ISysDictDataService { + + @Override + public TableDataInfo selectPageDictDataList(SysDictData dictData) { + LambdaQueryWrapper lqw = new LambdaQueryWrapper() + .eq(StrUtil.isNotBlank(dictData.getDictType()), SysDictData::getDictType, dictData.getDictType()) + .like(StrUtil.isNotBlank(dictData.getDictLabel()), SysDictData::getDictLabel, dictData.getDictLabel()) + .eq(StrUtil.isNotBlank(dictData.getStatus()), SysDictData::getStatus, dictData.getStatus()) + .orderByAsc(SysDictData::getDictSort); + return PageUtils.buildDataInfo(page(PageUtils.buildPage(), lqw)); + } + + /** + * 根据条件分页查询字典数据 + * + * @param dictData 字典数据信息 + * @return 字典数据集合信息 + */ + @Override + public List selectDictDataList(SysDictData dictData) { + return list(new LambdaQueryWrapper() + .eq(StrUtil.isNotBlank(dictData.getDictType()), SysDictData::getDictType, dictData.getDictType()) + .like(StrUtil.isNotBlank(dictData.getDictLabel()), SysDictData::getDictLabel, dictData.getDictLabel()) + .eq(StrUtil.isNotBlank(dictData.getStatus()), SysDictData::getStatus, dictData.getStatus()) + .orderByAsc(SysDictData::getDictSort)); + } + + /** + * 根据字典类型和字典键值查询字典数据信息 + * + * @param dictType 字典类型 + * @param dictValue 字典键值 + * @return 字典标签 + */ + @Override + public String selectDictLabel(String dictType, String dictValue) { + return getOne(new LambdaQueryWrapper() + .select(SysDictData::getDictLabel) + .eq(SysDictData::getDictType, dictType) + .eq(SysDictData::getDictValue, dictValue)) + .getDictLabel(); + } + + /** + * 根据字典数据ID查询信息 + * + * @param dictCode 字典数据ID + * @return 字典数据 + */ + @Override + public SysDictData selectDictDataById(Long dictCode) { + return getById(dictCode); + } + + /** + * 批量删除字典数据信息 + * + * @param dictCodes 需要删除的字典数据ID + * @return 结果 + */ + @Override + public void deleteDictDataByIds(Long[] dictCodes) { + for (Long dictCode : dictCodes) { + SysDictData data = selectDictDataById(dictCode); + List dictDatas = baseMapper.selectDictDataByType(data.getDictType()); + DictUtils.setDictCache(data.getDictType(), dictDatas); + } + baseMapper.deleteBatchIds(Arrays.asList(dictCodes)); + } + + /** + * 新增保存字典数据信息 + * + * @param data 字典数据信息 + * @return 结果 + */ + @Override + public int insertDictData(SysDictData data) { + int row = baseMapper.insert(data); + if (row > 0) { + List dictDatas = baseMapper.selectDictDataByType(data.getDictType()); + DictUtils.setDictCache(data.getDictType(), dictDatas); + } + return row; + } + + /** + * 修改保存字典数据信息 + * + * @param data 字典数据信息 + * @return 结果 + */ + @Override + public int updateDictData(SysDictData data) { + int row = baseMapper.updateById(data); + if (row > 0) { + List dictDatas = baseMapper.selectDictDataByType(data.getDictType()); + DictUtils.setDictCache(data.getDictType(), dictDatas); + } + return row; + } +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/impl/SysDictTypeServiceImpl.java b/bashi-system/src/main/java/com/bashi/system/service/impl/SysDictTypeServiceImpl.java new file mode 100644 index 0000000..0790fce --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/impl/SysDictTypeServiceImpl.java @@ -0,0 +1,237 @@ +package com.bashi.system.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bashi.common.constant.UserConstants; +import com.bashi.common.core.domain.entity.SysDictData; +import com.bashi.common.core.domain.entity.SysDictType; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.exception.CustomException; +import com.bashi.common.utils.DictUtils; +import com.bashi.common.utils.PageUtils; +import com.bashi.system.mapper.SysDictDataMapper; +import com.bashi.system.mapper.SysDictTypeMapper; +import com.bashi.system.service.ISysDictTypeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.PostConstruct; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * 字典 业务层处理 + * + * @author duteliang + */ +@Service +public class SysDictTypeServiceImpl extends ServiceImpl implements ISysDictTypeService { + + @Autowired + private SysDictDataMapper dictDataMapper; + + /** + * 项目启动时,初始化字典到缓存 + */ + @PostConstruct + public void init() { + loadingDictCache(); + } + + @Override + public TableDataInfo selectPageDictTypeList(SysDictType dictType) { + Map params = dictType.getParams(); + LambdaQueryWrapper lqw = new LambdaQueryWrapper() + .like(StrUtil.isNotBlank(dictType.getDictName()), SysDictType::getDictName, dictType.getDictName()) + .eq(StrUtil.isNotBlank(dictType.getStatus()), SysDictType::getStatus, dictType.getStatus()) + .like(StrUtil.isNotBlank(dictType.getDictType()), SysDictType::getDictType, dictType.getDictType()) + .apply(Validator.isNotEmpty(params.get("beginTime")), + "date_format(create_time,'%y%m%d') >= date_format({0},'%y%m%d')", + params.get("beginTime")) + .apply(Validator.isNotEmpty(params.get("endTime")), + "date_format(create_time,'%y%m%d') <= date_format({0},'%y%m%d')", + params.get("endTime")); + return PageUtils.buildDataInfo(page(PageUtils.buildPage(), lqw)); + } + + /** + * 根据条件分页查询字典类型 + * + * @param dictType 字典类型信息 + * @return 字典类型集合信息 + */ + @Override + public List selectDictTypeList(SysDictType dictType) { + Map params = dictType.getParams(); + return list(new LambdaQueryWrapper() + .like(StrUtil.isNotBlank(dictType.getDictName()), SysDictType::getDictName, dictType.getDictName()) + .eq(StrUtil.isNotBlank(dictType.getStatus()), SysDictType::getStatus, dictType.getStatus()) + .like(StrUtil.isNotBlank(dictType.getDictType()), SysDictType::getDictType, dictType.getDictType()) + .apply(Validator.isNotEmpty(params.get("beginTime")), + "date_format(create_time,'%y%m%d') >= date_format({0},'%y%m%d')", + params.get("beginTime")) + .apply(Validator.isNotEmpty(params.get("endTime")), + "date_format(create_time,'%y%m%d') <= date_format({0},'%y%m%d')", + params.get("endTime"))); + } + + /** + * 根据所有字典类型 + * + * @return 字典类型集合信息 + */ + @Override + public List selectDictTypeAll() { + return list(); + } + + /** + * 根据字典类型查询字典数据 + * + * @param dictType 字典类型 + * @return 字典数据集合信息 + */ + @Override + public List selectDictDataByType(String dictType) { + List dictDatas = DictUtils.getDictCache(dictType); + if (CollUtil.isNotEmpty(dictDatas)) { + return dictDatas; + } + dictDatas = dictDataMapper.selectDictDataByType(dictType); + if (CollUtil.isNotEmpty(dictDatas)) { + DictUtils.setDictCache(dictType, dictDatas); + return dictDatas; + } + return null; + } + + /** + * 根据字典类型ID查询信息 + * + * @param dictId 字典类型ID + * @return 字典类型 + */ + @Override + public SysDictType selectDictTypeById(Long dictId) { + return getById(dictId); + } + + /** + * 根据字典类型查询信息 + * + * @param dictType 字典类型 + * @return 字典类型 + */ + @Override + public SysDictType selectDictTypeByType(String dictType) { + return getOne(new LambdaQueryWrapper().eq(SysDictType::getDictType, dictType)); + } + + /** + * 批量删除字典类型信息 + * + * @param dictIds 需要删除的字典ID + * @return 结果 + */ + @Override + public void deleteDictTypeByIds(Long[] dictIds) { + for (Long dictId : dictIds) { + SysDictType dictType = selectDictTypeById(dictId); + if (dictDataMapper.selectCount(new LambdaQueryWrapper() + .eq(SysDictData::getDictType, dictType.getDictType())) > 0) { + throw new CustomException(String.format("%1$s已分配,不能删除", dictType.getDictName())); + } + DictUtils.removeDictCache(dictType.getDictType()); + } + baseMapper.deleteBatchIds(Arrays.asList(dictIds)); + } + + /** + * 加载字典缓存数据 + */ + @Override + public void loadingDictCache() { + List dictTypeList = list(); + for (SysDictType dictType : dictTypeList) { + List dictDatas = dictDataMapper.selectDictDataByType(dictType.getDictType()); + DictUtils.setDictCache(dictType.getDictType(), dictDatas); + } + } + + /** + * 清空字典缓存数据 + */ + @Override + public void clearDictCache() { + DictUtils.clearDictCache(); + } + + /** + * 重置字典缓存数据 + */ + @Override + public void resetDictCache() { + clearDictCache(); + loadingDictCache(); + } + + /** + * 新增保存字典类型信息 + * + * @param dict 字典类型信息 + * @return 结果 + */ + @Override + public int insertDictType(SysDictType dict) { + int row = baseMapper.insert(dict); + if (row > 0) { + DictUtils.setDictCache(dict.getDictType(), null); + } + return row; + } + + /** + * 修改保存字典类型信息 + * + * @param dict 字典类型信息 + * @return 结果 + */ + @Override + @Transactional + public int updateDictType(SysDictType dict) { + SysDictType oldDict = getById(dict.getDictId()); + dictDataMapper.update(null, new LambdaUpdateWrapper() + .set(SysDictData::getDictType, dict.getDictType()) + .eq(SysDictData::getDictType, oldDict.getDictType())); + int row = baseMapper.updateById(dict); + if (row > 0) { + List dictDatas = dictDataMapper.selectDictDataByType(dict.getDictType()); + DictUtils.setDictCache(dict.getDictType(), dictDatas); + } + return row; + } + + /** + * 校验字典类型称是否唯一 + * + * @param dict 字典类型 + * @return 结果 + */ + @Override + public String checkDictTypeUnique(SysDictType dict) { + Long dictId = Validator.isNull(dict.getDictId()) ? -1L : dict.getDictId(); + SysDictType dictType = getOne(new LambdaQueryWrapper() + .eq(SysDictType::getDictType, dict.getDictType()) + .last("limit 1")); + if (Validator.isNotNull(dictType) && dictType.getDictId().longValue() != dictId.longValue()) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/impl/SysLogininforServiceImpl.java b/bashi-system/src/main/java/com/bashi/system/service/impl/SysLogininforServiceImpl.java new file mode 100644 index 0000000..18d7e8c --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/impl/SysLogininforServiceImpl.java @@ -0,0 +1,94 @@ +package com.bashi.system.service.impl; + +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.utils.PageUtils; +import com.bashi.system.domain.SysLogininfor; +import com.bashi.system.mapper.SysLogininforMapper; +import com.bashi.system.service.ISysLogininforService; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 系统访问日志情况信息 服务层处理 + * + * @author duteliang + */ +@Service +public class SysLogininforServiceImpl extends ServiceImpl implements ISysLogininforService { + + @Override + public TableDataInfo selectPageLogininforList(SysLogininfor logininfor) { + Map params = logininfor.getParams(); + LambdaQueryWrapper lqw = new LambdaQueryWrapper() + .like(StrUtil.isNotBlank(logininfor.getIpaddr()), SysLogininfor::getIpaddr, logininfor.getIpaddr()) + .eq(StrUtil.isNotBlank(logininfor.getStatus()), SysLogininfor::getStatus, logininfor.getStatus()) + .like(StrUtil.isNotBlank(logininfor.getUserName()), SysLogininfor::getUserName, logininfor.getUserName()) + .apply(Validator.isNotEmpty(params.get("beginTime")), + "date_format(login_time,'%y%m%d') >= date_format({0},'%y%m%d')", + params.get("beginTime")) + .apply(Validator.isNotEmpty(params.get("endTime")), + "date_format(login_time,'%y%m%d') <= date_format({0},'%y%m%d')", + params.get("endTime")); + return PageUtils.buildDataInfo(page(PageUtils.buildPage("info_id","desc"), lqw)); + } + + /** + * 新增系统登录日志 + * + * @param logininfor 访问日志对象 + */ + @Override + public void insertLogininfor(SysLogininfor logininfor) { + logininfor.setLoginTime(new Date()); + save(logininfor); + } + + /** + * 查询系统登录日志集合 + * + * @param logininfor 访问日志对象 + * @return 登录记录集合 + */ + @Override + public List selectLogininforList(SysLogininfor logininfor) { + Map params = logininfor.getParams(); + return list(new LambdaQueryWrapper() + .like(StrUtil.isNotBlank(logininfor.getIpaddr()),SysLogininfor::getIpaddr,logininfor.getIpaddr()) + .eq(StrUtil.isNotBlank(logininfor.getStatus()),SysLogininfor::getStatus,logininfor.getStatus()) + .like(StrUtil.isNotBlank(logininfor.getUserName()),SysLogininfor::getUserName,logininfor.getUserName()) + .apply(Validator.isNotEmpty(params.get("beginTime")), + "date_format(login_time,'%y%m%d') >= date_format({0},'%y%m%d')", + params.get("beginTime")) + .apply(Validator.isNotEmpty(params.get("endTime")), + "date_format(login_time,'%y%m%d') <= date_format({0},'%y%m%d')", + params.get("endTime")) + .orderByDesc(SysLogininfor::getInfoId)); + } + + /** + * 批量删除系统登录日志 + * + * @param infoIds 需要删除的登录日志ID + * @return + */ + @Override + public int deleteLogininforByIds(Long[] infoIds) { + return baseMapper.deleteBatchIds(Arrays.asList(infoIds)); + } + + /** + * 清空系统登录日志 + */ + @Override + public void cleanLogininfor() { + remove(new LambdaQueryWrapper<>()); + } +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/impl/SysMenuServiceImpl.java b/bashi-system/src/main/java/com/bashi/system/service/impl/SysMenuServiceImpl.java new file mode 100644 index 0000000..873ccda --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/impl/SysMenuServiceImpl.java @@ -0,0 +1,412 @@ +package com.bashi.system.service.impl; + +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bashi.common.constant.UserConstants; +import com.bashi.common.core.domain.TreeSelect; +import com.bashi.common.core.domain.entity.SysMenu; +import com.bashi.common.core.domain.entity.SysRole; +import com.bashi.common.core.domain.entity.SysUser; +import com.bashi.common.utils.SecurityUtils; +import com.bashi.system.domain.SysRoleMenu; +import com.bashi.system.domain.vo.MetaVo; +import com.bashi.system.domain.vo.RouterVo; +import com.bashi.system.mapper.SysMenuMapper; +import com.bashi.system.mapper.SysRoleMapper; +import com.bashi.system.mapper.SysRoleMenuMapper; +import com.bashi.system.service.ISysMenuService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 菜单 业务层处理 + * + * @author duteliang + */ +@Service +public class SysMenuServiceImpl extends ServiceImpl implements ISysMenuService { + public static final String PREMISSION_STRING = "perms[\"{0}\"]"; + + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysRoleMenuMapper roleMenuMapper; + + /** + * 根据用户查询系统菜单列表 + * + * @param userId 用户ID + * @return 菜单列表 + */ + @Override + public List selectMenuList(Long userId) { + return selectMenuList(new SysMenu(), userId); + } + + /** + * 查询系统菜单列表 + * + * @param menu 菜单信息 + * @return 菜单列表 + */ + @Override + public List selectMenuList(SysMenu menu, Long userId) { + List menuList = null; + // 管理员显示所有菜单信息 + if (SysUser.isAdmin(userId)) { + menuList = list(new LambdaQueryWrapper() + .like(StrUtil.isNotBlank(menu.getMenuName()),SysMenu::getMenuName,menu.getMenuName()) + .eq(StrUtil.isNotBlank(menu.getVisible()),SysMenu::getVisible,menu.getVisible()) + .eq(StrUtil.isNotBlank(menu.getStatus()),SysMenu::getStatus,menu.getStatus()) + .orderByAsc(SysMenu::getParentId) + .orderByAsc(SysMenu::getOrderNum)); + } else { + menu.getParams().put("userId", userId); + menuList = baseMapper.selectMenuListByUserId(menu); + } + return menuList; + } + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + @Override + public Set selectMenuPermsByUserId(Long userId) { + List perms = baseMapper.selectMenuPermsByUserId(userId); + Set permsSet = new HashSet<>(); + for (String perm : perms) { + if (Validator.isNotEmpty(perm)) { + permsSet.addAll(Arrays.asList(perm.trim().split(","))); + } + } + return permsSet; + } + + /** + * 根据用户ID查询菜单 + * + * @param userId 用户名称 + * @return 菜单列表 + */ + @Override + public List selectMenuTreeByUserId(Long userId) { + List menus = null; + if (SecurityUtils.isAdmin(userId)) { + menus = baseMapper.selectMenuTreeAll(); + } else { + menus = baseMapper.selectMenuTreeByUserId(userId); + } + return getChildPerms(menus, 0); + } + + /** + * 根据角色ID查询菜单树信息 + * + * @param roleId 角色ID + * @return 选中菜单列表 + */ + @Override + public List selectMenuListByRoleId(Long roleId) { + SysRole role = roleMapper.selectById(roleId); + return baseMapper.selectMenuListByRoleId(roleId, role.isMenuCheckStrictly()); + } + + /** + * 构建前端路由所需要的菜单 + * + * @param menus 菜单列表 + * @return 路由列表 + */ + @Override + public List buildMenus(List menus) { + List routers = new LinkedList(); + for (SysMenu menu : menus) { + RouterVo router = new RouterVo(); + router.setHidden("1".equals(menu.getVisible())); + router.setName(getRouteName(menu)); + router.setPath(getRouterPath(menu)); + router.setComponent(getComponent(menu)); + router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StrUtil.equals("1", menu.getIsCache()))); + List cMenus = menu.getChildren(); + if (!cMenus.isEmpty() && UserConstants.TYPE_DIR.equals(menu.getMenuType())) { + router.setAlwaysShow(true); + router.setRedirect("noRedirect"); + router.setChildren(buildMenus(cMenus)); + } else if (isMenuFrame(menu)) { + router.setMeta(null); + List childrenList = new ArrayList(); + RouterVo children = new RouterVo(); + children.setPath(menu.getPath()); + children.setComponent(menu.getComponent()); + children.setName(StrUtil.upperFirst(menu.getPath())); + children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StrUtil.equals("1", menu.getIsCache()))); + childrenList.add(children); + router.setChildren(childrenList); + } + routers.add(router); + } + return routers; + } + + /** + * 构建前端所需要树结构 + * + * @param menus 菜单列表 + * @return 树结构列表 + */ + @Override + public List buildMenuTree(List menus) { + List returnList = new ArrayList(); + List tempList = new ArrayList(); + for (SysMenu dept : menus) { + tempList.add(dept.getMenuId()); + } + for (SysMenu menu : menus) { + // 如果是顶级节点, 遍历该父节点的所有子节点 + if (!tempList.contains(menu.getParentId())) { + recursionFn(menus, menu); + returnList.add(menu); + } + } + if (returnList.isEmpty()) { + returnList = menus; + } + return returnList; + } + + /** + * 构建前端所需要下拉树结构 + * + * @param menus 菜单列表 + * @return 下拉树结构列表 + */ + @Override + public List buildMenuTreeSelect(List menus) { + List menuTrees = buildMenuTree(menus); + return menuTrees.stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + /** + * 根据菜单ID查询信息 + * + * @param menuId 菜单ID + * @return 菜单信息 + */ + @Override + public SysMenu selectMenuById(Long menuId) { + return getById(menuId); + } + + /** + * 是否存在菜单子节点 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public boolean hasChildByMenuId(Long menuId) { + long result = count(new LambdaQueryWrapper().eq(SysMenu::getParentId,menuId)); + return result > 0; + } + + /** + * 查询菜单使用数量 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public boolean checkMenuExistRole(Long menuId) { + long result = roleMenuMapper.selectCount(new LambdaQueryWrapper().eq(SysRoleMenu::getMenuId,menuId)); + return result > 0; + } + + /** + * 新增保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public int insertMenu(SysMenu menu) { + return baseMapper.insert(menu); + } + + /** + * 修改保存菜单信息 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public int updateMenu(SysMenu menu) { + return baseMapper.updateById(menu); + } + + /** + * 删除菜单管理信息 + * + * @param menuId 菜单ID + * @return 结果 + */ + @Override + public int deleteMenuById(Long menuId) { + return baseMapper.deleteById(menuId); + } + + /** + * 校验菜单名称是否唯一 + * + * @param menu 菜单信息 + * @return 结果 + */ + @Override + public String checkMenuNameUnique(SysMenu menu) { + Long menuId = Validator.isNull(menu.getMenuId()) ? -1L : menu.getMenuId(); + SysMenu info = getOne(new LambdaQueryWrapper() + .eq(SysMenu::getMenuName,menu.getMenuName()) + .eq(SysMenu::getParentId,menu.getParentId()) + .last("limit 1")); + if (Validator.isNotNull(info) && info.getMenuId().longValue() != menuId.longValue()) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 获取路由名称 + * + * @param menu 菜单信息 + * @return 路由名称 + */ + public String getRouteName(SysMenu menu) { + String routerName = StrUtil.upperFirst(menu.getPath()); + // 非外链并且是一级目录(类型为目录) + if (isMenuFrame(menu)) { + routerName = StrUtil.EMPTY; + } + return routerName; + } + + /** + * 获取路由地址 + * + * @param menu 菜单信息 + * @return 路由地址 + */ + public String getRouterPath(SysMenu menu) { + String routerPath = menu.getPath(); + // 非外链并且是一级目录(类型为目录) + if (0 == menu.getParentId().intValue() && UserConstants.TYPE_DIR.equals(menu.getMenuType()) + && UserConstants.NO_FRAME.equals(menu.getIsFrame())) { + routerPath = "/" + menu.getPath(); + } + // 非外链并且是一级目录(类型为菜单) + else if (isMenuFrame(menu)) { + routerPath = "/"; + } + return routerPath; + } + + /** + * 获取组件信息 + * + * @param menu 菜单信息 + * @return 组件信息 + */ + public String getComponent(SysMenu menu) { + String component = UserConstants.LAYOUT; + if (StrUtil.isNotEmpty(menu.getComponent()) && !isMenuFrame(menu)) { + component = menu.getComponent(); + } else if (StrUtil.isEmpty(menu.getComponent()) && isParentView(menu)) { + component = UserConstants.PARENT_VIEW; + } + return component; + } + + /** + * 是否为菜单内部跳转 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isMenuFrame(SysMenu menu) { + return menu.getParentId().intValue() == 0 && UserConstants.TYPE_MENU.equals(menu.getMenuType()) + && menu.getIsFrame().equals(UserConstants.NO_FRAME); + } + + /** + * 是否为parent_view组件 + * + * @param menu 菜单信息 + * @return 结果 + */ + public boolean isParentView(SysMenu menu) { + return menu.getParentId().intValue() != 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType()); + } + + /** + * 根据父节点的ID获取所有子节点 + * + * @param list 分类表 + * @param parentId 传入的父节点ID + * @return String + */ + public List getChildPerms(List list, int parentId) { + List returnList = new ArrayList(); + for (SysMenu t : list) { + // 一、根据传入的某个父节点ID,遍历该父节点的所有子节点 + if (t.getParentId() == parentId) { + recursionFn(list, t); + returnList.add(t); + } + } + return returnList; + } + + /** + * 递归列表 + * + * @param list + * @param t + */ + private void recursionFn(List list, SysMenu t) { + // 得到子节点列表 + List childList = getChildList(list, t); + t.setChildren(childList); + for (SysMenu tChild : childList) { + if (hasChild(list, tChild)) { + recursionFn(list, tChild); + } + } + } + + /** + * 得到子节点列表 + */ + private List getChildList(List list, SysMenu t) { + List tlist = new ArrayList(); + for (SysMenu n : list) { + if (n.getParentId().longValue() == t.getMenuId().longValue()) { + tlist.add(n); + } + } + return tlist; + } + + /** + * 判断是否有子节点 + */ + private boolean hasChild(List list, SysMenu t) { + return getChildList(list, t).size() > 0; + } +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/impl/SysNoticeServiceImpl.java b/bashi-system/src/main/java/com/bashi/system/service/impl/SysNoticeServiceImpl.java new file mode 100644 index 0000000..ea128a0 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/impl/SysNoticeServiceImpl.java @@ -0,0 +1,101 @@ +package com.bashi.system.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.utils.PageUtils; +import com.bashi.system.domain.SysNotice; +import com.bashi.system.mapper.SysNoticeMapper; +import com.bashi.system.service.ISysNoticeService; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.List; + +/** + * 公告 服务层实现 + * + * @author duteliang + */ +@Service +public class SysNoticeServiceImpl extends ServiceImpl implements ISysNoticeService { + + @Override + public TableDataInfo selectPageNoticeList(SysNotice notice) { + LambdaQueryWrapper lqw = new LambdaQueryWrapper() + .like(StrUtil.isNotBlank(notice.getNoticeTitle()), SysNotice::getNoticeTitle, notice.getNoticeTitle()) + .eq(StrUtil.isNotBlank(notice.getNoticeType()), SysNotice::getNoticeType, notice.getNoticeType()) + .like(StrUtil.isNotBlank(notice.getCreateBy()), SysNotice::getCreateBy, notice.getCreateBy()); + return PageUtils.buildDataInfo(page(PageUtils.buildPage(),lqw)); + } + + /** + * 查询公告信息 + * + * @param noticeId 公告ID + * @return 公告信息 + */ + @Override + public SysNotice selectNoticeById(Long noticeId) { + return getById(noticeId); + } + + /** + * 查询公告列表 + * + * @param notice 公告信息 + * @return 公告集合 + */ + @Override + public List selectNoticeList(SysNotice notice) { + return list(new LambdaQueryWrapper() + .like(StrUtil.isNotBlank(notice.getNoticeTitle()),SysNotice::getNoticeTitle,notice.getNoticeTitle()) + .eq(StrUtil.isNotBlank(notice.getNoticeType()),SysNotice::getNoticeType,notice.getNoticeType()) + .like(StrUtil.isNotBlank(notice.getCreateBy()),SysNotice::getCreateBy,notice.getCreateBy())); + } + + /** + * 新增公告 + * + * @param notice 公告信息 + * @return 结果 + */ + @Override + public int insertNotice(SysNotice notice) { + return baseMapper.insert(notice); + } + + /** + * 修改公告 + * + * @param notice 公告信息 + * @return 结果 + */ + @Override + public int updateNotice(SysNotice notice) { + return baseMapper.updateById(notice); + } + + /** + * 删除公告对象 + * + * @param noticeId 公告ID + * @return 结果 + */ + @Override + public int deleteNoticeById(Long noticeId) { + return baseMapper.deleteById(noticeId); + } + + /** + * 批量删除公告信息 + * + * @param noticeIds 需要删除的公告ID + * @return 结果 + */ + @Override + public int deleteNoticeByIds(Long[] noticeIds) { + return baseMapper.deleteBatchIds(Arrays.asList(noticeIds)); + } +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/impl/SysOperLogServiceImpl.java b/bashi-system/src/main/java/com/bashi/system/service/impl/SysOperLogServiceImpl.java new file mode 100644 index 0000000..7c0cdfb --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/impl/SysOperLogServiceImpl.java @@ -0,0 +1,122 @@ +package com.bashi.system.service.impl; + +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.utils.PageUtils; +import com.bashi.system.domain.SysOperLog; +import com.bashi.system.mapper.SysOperLogMapper; +import com.bashi.system.service.ISysOperLogService; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 操作日志 服务层处理 + * + * @author duteliang + */ +@Service +public class SysOperLogServiceImpl extends ServiceImpl implements ISysOperLogService { + + @Override + public TableDataInfo selectPageOperLogList(SysOperLog operLog) { + Map params = operLog.getParams(); + LambdaQueryWrapper lqw = new LambdaQueryWrapper() + .like(StrUtil.isNotBlank(operLog.getTitle()), SysOperLog::getTitle, operLog.getTitle()) + .eq(operLog.getBusinessType() != null && operLog.getBusinessType() > 0, + SysOperLog::getBusinessType, operLog.getBusinessType()) + .func(f -> { + if (ArrayUtil.isNotEmpty(operLog.getBusinessTypes())) { + f.in(SysOperLog::getBusinessType, Arrays.asList(operLog.getBusinessTypes())); + } + }) + .eq(operLog.getStatus() != null && operLog.getStatus() > 0, + SysOperLog::getStatus, operLog.getStatus()) + .like(StrUtil.isNotBlank(operLog.getOperName()), SysOperLog::getOperName, operLog.getOperName()) + .apply(Validator.isNotEmpty(params.get("beginTime")), + "date_format(oper_time,'%y%m%d') >= date_format({0},'%y%m%d')", + params.get("beginTime")) + .apply(Validator.isNotEmpty(params.get("endTime")), + "date_format(oper_time,'%y%m%d') <= date_format({0},'%y%m%d')", + params.get("endTime")); + return PageUtils.buildDataInfo(page(PageUtils.buildPage("oper_id","desc"), lqw)); + } + + /** + * 新增操作日志 + * + * @param operLog 操作日志对象 + */ + @Override + public void insertOperlog(SysOperLog operLog) { + operLog.setOperTime(new Date()); + save(operLog); + } + + /** + * 查询系统操作日志集合 + * + * @param operLog 操作日志对象 + * @return 操作日志集合 + */ + @Override + public List selectOperLogList(SysOperLog operLog) { + Map params = operLog.getParams(); + return list(new LambdaQueryWrapper() + .like(StrUtil.isNotBlank(operLog.getTitle()),SysOperLog::getTitle,operLog.getTitle()) + .eq(operLog.getBusinessType() != null && operLog.getBusinessType() > 0, + SysOperLog::getBusinessType,operLog.getBusinessType()) + .func(f -> { + if (ArrayUtil.isNotEmpty(operLog.getBusinessTypes())){ + f.in(SysOperLog::getBusinessType, Arrays.asList(operLog.getBusinessTypes())); + } + }) + .eq(operLog.getStatus() != null && operLog.getStatus() > 0, + SysOperLog::getStatus,operLog.getStatus()) + .like(StrUtil.isNotBlank(operLog.getOperName()),SysOperLog::getOperName,operLog.getOperName()) + .apply(Validator.isNotEmpty(params.get("beginTime")), + "date_format(oper_time,'%y%m%d') >= date_format({0},'%y%m%d')", + params.get("beginTime")) + .apply(Validator.isNotEmpty(params.get("endTime")), + "date_format(oper_time,'%y%m%d') <= date_format({0},'%y%m%d')", + params.get("endTime")) + .orderByDesc(SysOperLog::getOperId)); + } + + /** + * 批量删除系统操作日志 + * + * @param operIds 需要删除的操作日志ID + * @return 结果 + */ + @Override + public int deleteOperLogByIds(Long[] operIds) { + return baseMapper.deleteBatchIds(Arrays.asList(operIds)); + } + + /** + * 查询操作日志详细 + * + * @param operId 操作ID + * @return 操作日志对象 + */ + @Override + public SysOperLog selectOperLogById(Long operId) { + return getById(operId); + } + + /** + * 清空操作日志 + */ + @Override + public void cleanOperLog() { + remove(new LambdaQueryWrapper<>()); + } +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/impl/SysPostServiceImpl.java b/bashi-system/src/main/java/com/bashi/system/service/impl/SysPostServiceImpl.java new file mode 100644 index 0000000..a7d869b --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/impl/SysPostServiceImpl.java @@ -0,0 +1,183 @@ +package com.bashi.system.service.impl; + +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bashi.common.constant.UserConstants; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.exception.CustomException; +import com.bashi.common.utils.PageUtils; +import com.bashi.system.domain.SysPost; +import com.bashi.system.domain.SysUserPost; +import com.bashi.system.mapper.SysPostMapper; +import com.bashi.system.mapper.SysUserPostMapper; +import com.bashi.system.service.ISysPostService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.List; + +/** + * 岗位信息 服务层处理 + * + * @author duteliang + */ +@Service +public class SysPostServiceImpl extends ServiceImpl implements ISysPostService { + + @Autowired + private SysUserPostMapper userPostMapper; + + @Override + public TableDataInfo selectPagePostList(SysPost post) { + LambdaQueryWrapper lqw = new LambdaQueryWrapper() + .like(StrUtil.isNotBlank(post.getPostCode()), SysPost::getPostCode, post.getPostCode()) + .eq(StrUtil.isNotBlank(post.getStatus()), SysPost::getStatus, post.getStatus()) + .like(StrUtil.isNotBlank(post.getPostName()), SysPost::getPostName, post.getPostName()); + return PageUtils.buildDataInfo(page(PageUtils.buildPage(),lqw)); + } + + /** + * 查询岗位信息集合 + * + * @param post 岗位信息 + * @return 岗位信息集合 + */ + @Override + public List selectPostList(SysPost post) { + return list(new LambdaQueryWrapper() + .like(StrUtil.isNotBlank(post.getPostCode()), SysPost::getPostCode, post.getPostCode()) + .eq(StrUtil.isNotBlank(post.getStatus()), SysPost::getStatus, post.getStatus()) + .like(StrUtil.isNotBlank(post.getPostName()), SysPost::getPostName, post.getPostName())); + } + + /** + * 查询所有岗位 + * + * @return 岗位列表 + */ + @Override + public List selectPostAll() { + return list(); + } + + /** + * 通过岗位ID查询岗位信息 + * + * @param postId 岗位ID + * @return 角色对象信息 + */ + @Override + public SysPost selectPostById(Long postId) { + return getById(postId); + } + + /** + * 根据用户ID获取岗位选择框列表 + * + * @param userId 用户ID + * @return 选中岗位ID列表 + */ + @Override + public List selectPostListByUserId(Long userId) { + return baseMapper.selectPostListByUserId(userId); + } + + /** + * 校验岗位名称是否唯一 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public String checkPostNameUnique(SysPost post) { + Long postId = Validator.isNull(post.getPostId()) ? -1L : post.getPostId(); + SysPost info = getOne(new LambdaQueryWrapper() + .eq(SysPost::getPostName, post.getPostName()).last("limit 1")); + if (Validator.isNotNull(info) && info.getPostId().longValue() != postId.longValue()) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验岗位编码是否唯一 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public String checkPostCodeUnique(SysPost post) { + Long postId = Validator.isNull(post.getPostId()) ? -1L : post.getPostId(); + SysPost info = getOne(new LambdaQueryWrapper() + .eq(SysPost::getPostCode, post.getPostCode()).last("limit 1")); + if (Validator.isNotNull(info) && info.getPostId().longValue() != postId.longValue()) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 通过岗位ID查询岗位使用数量 + * + * @param postId 岗位ID + * @return 结果 + */ + @Override + public long countUserPostById(Long postId) { + return userPostMapper.selectCount(new LambdaQueryWrapper().eq(SysUserPost::getPostId,postId)); + } + + /** + * 删除岗位信息 + * + * @param postId 岗位ID + * @return 结果 + */ + @Override + public int deletePostById(Long postId) { + return baseMapper.deleteById(postId); + } + + /** + * 批量删除岗位信息 + * + * @param postIds 需要删除的岗位ID + * @return 结果 + * @throws Exception 异常 + */ + @Override + public int deletePostByIds(Long[] postIds) { + for (Long postId : postIds) { + SysPost post = selectPostById(postId); + if (countUserPostById(postId) > 0) { + throw new CustomException(String.format("%1$s已分配,不能删除", post.getPostName())); + } + } + return baseMapper.deleteBatchIds(Arrays.asList(postIds)); + } + + /** + * 新增保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public int insertPost(SysPost post) { + return baseMapper.insert(post); + } + + /** + * 修改保存岗位信息 + * + * @param post 岗位信息 + * @return 结果 + */ + @Override + public int updatePost(SysPost post) { + return baseMapper.updateById(post); + } +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/impl/SysRoleServiceImpl.java b/bashi-system/src/main/java/com/bashi/system/service/impl/SysRoleServiceImpl.java new file mode 100644 index 0000000..6035a96 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/impl/SysRoleServiceImpl.java @@ -0,0 +1,308 @@ +package com.bashi.system.service.impl; + +import cn.hutool.core.lang.Validator; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bashi.common.annotation.DataScope; +import com.bashi.common.constant.UserConstants; +import com.bashi.common.core.domain.entity.SysRole; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.exception.CustomException; +import com.bashi.common.utils.PageUtils; +import com.bashi.common.utils.spring.SpringUtils; +import com.bashi.system.domain.SysRoleDept; +import com.bashi.system.domain.SysRoleMenu; +import com.bashi.system.domain.SysUserRole; +import com.bashi.system.mapper.SysRoleDeptMapper; +import com.bashi.system.mapper.SysRoleMapper; +import com.bashi.system.mapper.SysRoleMenuMapper; +import com.bashi.system.mapper.SysUserRoleMapper; +import com.bashi.system.service.ISysRoleService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; + +/** + * 角色 业务层处理 + * + * @author duteliang + */ +@Service +public class SysRoleServiceImpl extends ServiceImpl implements ISysRoleService { + + @Autowired + private SysRoleMenuMapper roleMenuMapper; + + @Autowired + private SysUserRoleMapper userRoleMapper; + + @Autowired + private SysRoleDeptMapper roleDeptMapper; + + @Override + @DataScope(deptAlias = "d") + public TableDataInfo selectPageRoleList(SysRole role) { + return PageUtils.buildDataInfo(baseMapper.selectPageRoleList(PageUtils.buildPage(), role)); + } + + /** + * 根据条件分页查询角色数据 + * + * @param role 角色信息 + * @return 角色数据集合信息 + */ + @Override + @DataScope(deptAlias = "d") + public List selectRoleList(SysRole role) { + return baseMapper.selectRoleList(role); + } + + /** + * 根据用户ID查询权限 + * + * @param userId 用户ID + * @return 权限列表 + */ + @Override + public Set selectRolePermissionByUserId(Long userId) { + List perms = baseMapper.selectRolePermissionByUserId(userId); + Set permsSet = new HashSet<>(); + for (SysRole perm : perms) { + if (Validator.isNotNull(perm)) { + permsSet.addAll(Arrays.asList(perm.getRoleKey().trim().split(","))); + } + } + return permsSet; + } + + /** + * 查询所有角色 + * + * @return 角色列表 + */ + @Override + public List selectRoleAll() { + return SpringUtils.getAopProxy(this).selectRoleList(new SysRole()); + } + + /** + * 根据用户ID获取角色选择框列表 + * + * @param userId 用户ID + * @return 选中角色ID列表 + */ + @Override + public List selectRoleListByUserId(Long userId) { + return baseMapper.selectRoleListByUserId(userId); + } + + /** + * 通过角色ID查询角色 + * + * @param roleId 角色ID + * @return 角色对象信息 + */ + @Override + public SysRole selectRoleById(Long roleId) { + return getById(roleId); + } + + /** + * 校验角色名称是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public String checkRoleNameUnique(SysRole role) { + Long roleId = Validator.isNull(role.getRoleId()) ? -1L : role.getRoleId(); + SysRole info = getOne(new LambdaQueryWrapper() + .eq(SysRole::getRoleName, role.getRoleName()).last("limit 1")); + if (Validator.isNotNull(info) && info.getRoleId().longValue() != roleId.longValue()) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验角色权限是否唯一 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public String checkRoleKeyUnique(SysRole role) { + Long roleId = Validator.isNull(role.getRoleId()) ? -1L : role.getRoleId(); + SysRole info = getOne(new LambdaQueryWrapper() + .eq(SysRole::getRoleKey, role.getRoleKey()).last("limit 1")); + if (Validator.isNotNull(info) && info.getRoleId().longValue() != roleId.longValue()) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验角色是否允许操作 + * + * @param role 角色信息 + */ + @Override + public void checkRoleAllowed(SysRole role) { + if (Validator.isNotNull(role.getRoleId()) && role.isAdmin()) { + throw new CustomException("不允许操作超级管理员角色"); + } + } + + /** + * 通过角色ID查询角色使用数量 + * + * @param roleId 角色ID + * @return 结果 + */ + @Override + public long countUserRoleByRoleId(Long roleId) { + return userRoleMapper.selectCount(new LambdaQueryWrapper().eq(SysUserRole::getRoleId, roleId)); + } + + /** + * 新增保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional + public int insertRole(SysRole role) { + // 新增角色信息 + baseMapper.insert(role); + return insertRoleMenu(role); + } + + /** + * 修改保存角色信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional + public int updateRole(SysRole role) { + // 修改角色信息 + baseMapper.updateById(role); + // 删除角色与菜单关联 + roleMenuMapper.delete(new LambdaQueryWrapper().eq(SysRoleMenu::getRoleId, role.getRoleId())); + return insertRoleMenu(role); + } + + /** + * 修改角色状态 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + public int updateRoleStatus(SysRole role) { + return baseMapper.updateById(role); + } + + /** + * 修改数据权限信息 + * + * @param role 角色信息 + * @return 结果 + */ + @Override + @Transactional + public int authDataScope(SysRole role) { + // 修改角色信息 + baseMapper.updateById(role); + // 删除角色与部门关联 + roleDeptMapper.delete(new LambdaQueryWrapper().eq(SysRoleDept::getRoleId, role.getRoleId())); + // 新增角色和部门信息(数据权限) + return insertRoleDept(role); + } + + /** + * 新增角色菜单信息 + * + * @param role 角色对象 + */ + public int insertRoleMenu(SysRole role) { + int rows = 1; + // 新增用户与角色管理 + List list = new ArrayList(); + for (Long menuId : role.getMenuIds()) { + SysRoleMenu rm = new SysRoleMenu(); + rm.setRoleId(role.getRoleId()); + rm.setMenuId(menuId); + list.add(rm); + } + if (list.size() > 0) { + rows = roleMenuMapper.insertAll(list); + } + return rows; + } + + /** + * 新增角色部门信息(数据权限) + * + * @param role 角色对象 + */ + public int insertRoleDept(SysRole role) { + int rows = 1; + // 新增角色与部门(数据权限)管理 + List list = new ArrayList(); + for (Long deptId : role.getDeptIds()) { + SysRoleDept rd = new SysRoleDept(); + rd.setRoleId(role.getRoleId()); + rd.setDeptId(deptId); + list.add(rd); + } + if (list.size() > 0) { + rows = roleDeptMapper.insertAll(list); + } + return rows; + } + + /** + * 通过角色ID删除角色 + * + * @param roleId 角色ID + * @return 结果 + */ + @Override + @Transactional + public int deleteRoleById(Long roleId) { + // 删除角色与菜单关联 + roleMenuMapper.delete(new LambdaQueryWrapper().eq(SysRoleMenu::getRoleId, roleId)); + // 删除角色与部门关联 + roleDeptMapper.delete(new LambdaQueryWrapper().eq(SysRoleDept::getRoleId, roleId)); + return baseMapper.deleteById(roleId); + } + + /** + * 批量删除角色信息 + * + * @param roleIds 需要删除的角色ID + * @return 结果 + */ + @Override + @Transactional + public int deleteRoleByIds(Long[] roleIds) { + for (Long roleId : roleIds) { + checkRoleAllowed(new SysRole(roleId)); + SysRole role = selectRoleById(roleId); + if (countUserRoleByRoleId(roleId) > 0) { + throw new CustomException(String.format("%1$s已分配,不能删除", role.getRoleName())); + } + } + List ids = Arrays.asList(roleIds); + // 删除角色与菜单关联 + roleMenuMapper.delete(new LambdaQueryWrapper().in(SysRoleMenu::getRoleId, ids)); + // 删除角色与部门关联 + roleDeptMapper.delete(new LambdaQueryWrapper().in(SysRoleDept::getRoleId, ids)); + return baseMapper.deleteBatchIds(ids); + } +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/impl/SysUserOnlineServiceImpl.java b/bashi-system/src/main/java/com/bashi/system/service/impl/SysUserOnlineServiceImpl.java new file mode 100644 index 0000000..2cc7ee9 --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/impl/SysUserOnlineServiceImpl.java @@ -0,0 +1,87 @@ +package com.bashi.system.service.impl; + +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.StrUtil; +import com.bashi.common.core.domain.model.LoginUser; +import com.bashi.system.domain.SysUserOnline; +import com.bashi.system.service.ISysUserOnlineService; +import org.springframework.stereotype.Service; + +/** + * 在线用户 服务层处理 + * + * @author duteliang + */ +@Service +public class SysUserOnlineServiceImpl implements ISysUserOnlineService { + /** + * 通过登录地址查询信息 + * + * @param ipaddr 登录地址 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByIpaddr(String ipaddr, LoginUser user) { + if (StrUtil.equals(ipaddr, user.getIpaddr())) { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 通过用户名称查询信息 + * + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByUserName(String userName, LoginUser user) { + if (StrUtil.equals(userName, user.getUsername())) { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 通过登录地址/用户名称查询信息 + * + * @param ipaddr 登录地址 + * @param userName 用户名称 + * @param user 用户信息 + * @return 在线用户信息 + */ + @Override + public SysUserOnline selectOnlineByInfo(String ipaddr, String userName, LoginUser user) { + if (StrUtil.equals(ipaddr, user.getIpaddr()) && StrUtil.equals(userName, user.getUsername())) { + return loginUserToUserOnline(user); + } + return null; + } + + /** + * 设置在线用户信息 + * + * @param user 用户信息 + * @return 在线用户 + */ + @Override + public SysUserOnline loginUserToUserOnline(LoginUser user) { + if (Validator.isNull(user) || Validator.isNull(user.getUser())) { + return null; + } + SysUserOnline sysUserOnline = new SysUserOnline(); + sysUserOnline.setTokenId(user.getToken()); + sysUserOnline.setUserName(user.getUsername()); + sysUserOnline.setIpaddr(user.getIpaddr()); + sysUserOnline.setLoginLocation(user.getLoginLocation()); + sysUserOnline.setBrowser(user.getBrowser()); + sysUserOnline.setOs(user.getOs()); + sysUserOnline.setLoginTime(user.getLoginTime()); + if (Validator.isNotNull(user.getUser().getDept())) { + sysUserOnline.setDeptName(user.getUser().getDept().getDeptName()); + } + return sysUserOnline; + } +} diff --git a/bashi-system/src/main/java/com/bashi/system/service/impl/SysUserServiceImpl.java b/bashi-system/src/main/java/com/bashi/system/service/impl/SysUserServiceImpl.java new file mode 100644 index 0000000..0584c8e --- /dev/null +++ b/bashi-system/src/main/java/com/bashi/system/service/impl/SysUserServiceImpl.java @@ -0,0 +1,442 @@ +package com.bashi.system.service.impl; + +import cn.hutool.core.lang.Validator; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bashi.common.annotation.DataScope; +import com.bashi.common.constant.UserConstants; +import com.bashi.common.core.domain.entity.SysRole; +import com.bashi.common.core.domain.entity.SysUser; +import com.bashi.common.core.page.TableDataInfo; +import com.bashi.common.exception.CustomException; +import com.bashi.common.utils.PageUtils; +import com.bashi.common.utils.SecurityUtils; +import com.bashi.system.domain.SysPost; +import com.bashi.system.domain.SysUserPost; +import com.bashi.system.domain.SysUserRole; +import com.bashi.system.mapper.*; +import com.bashi.system.service.ISysConfigService; +import com.bashi.system.service.ISysUserService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 用户 业务层处理 + * + * @author duteliang + */ +@Slf4j +@Service +public class SysUserServiceImpl extends ServiceImpl implements ISysUserService { + + @Autowired + private SysRoleMapper roleMapper; + + @Autowired + private SysPostMapper postMapper; + + @Autowired + private SysUserRoleMapper userRoleMapper; + + @Autowired + private SysUserPostMapper userPostMapper; + + @Autowired + private ISysConfigService configService; + + @Override + @DataScope(deptAlias = "d", userAlias = "u", isUser = true) + public TableDataInfo selectPageUserList(SysUser user) { + return PageUtils.buildDataInfo(baseMapper.selectPageUserList(PageUtils.buildPage(), user)); + } + + /** + * 根据条件分页查询用户列表 + * + * @param user 用户信息 + * @return 用户信息集合信息 + */ + @Override + @DataScope(deptAlias = "d", userAlias = "u", isUser = true) + public List selectUserList(SysUser user) { + return baseMapper.selectUserList(user); + } + + /** + * 通过用户名查询用户 + * + * @param userName 用户名 + * @return 用户对象信息 + */ + @Override + public SysUser selectUserByUserName(String userName) { + return baseMapper.selectUserByUserName(userName); + } + + /** + * 通过用户ID查询用户 + * + * @param userId 用户ID + * @return 用户对象信息 + */ + @Override + public SysUser selectUserById(Long userId) { + return baseMapper.selectUserById(userId); + } + + /** + * 查询用户所属角色组 + * + * @param userName 用户名 + * @return 结果 + */ + @Override + public String selectUserRoleGroup(String userName) { + List list = roleMapper.selectRolesByUserName(userName); + StringBuilder idsStr = new StringBuilder(); + for (SysRole role : list) { + idsStr.append(role.getRoleName()).append(","); + } + if (Validator.isNotEmpty(idsStr.toString())) { + return idsStr.substring(0, idsStr.length() - 1); + } + return idsStr.toString(); + } + + /** + * 查询用户所属岗位组 + * + * @param userName 用户名 + * @return 结果 + */ + @Override + public String selectUserPostGroup(String userName) { + List list = postMapper.selectPostsByUserName(userName); + StringBuilder idsStr = new StringBuilder(); + for (SysPost post : list) { + idsStr.append(post.getPostName()).append(","); + } + if (Validator.isNotEmpty(idsStr.toString())) { + return idsStr.substring(0, idsStr.length() - 1); + } + return idsStr.toString(); + } + + /** + * 校验用户名称是否唯一 + * + * @param userName 用户名称 + * @return 结果 + */ + @Override + public String checkUserNameUnique(String userName) { + long count = count(new LambdaQueryWrapper().eq(SysUser::getUserName, userName).last("limit 1")); + if (count > 0) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验用户名称是否唯一 + * + * @param user 用户信息 + * @return + */ + @Override + public String checkPhoneUnique(SysUser user) { + Long userId = Validator.isNull(user.getUserId()) ? -1L : user.getUserId(); + SysUser info = getOne(new LambdaQueryWrapper() + .select(SysUser::getUserId, SysUser::getPhonenumber) + .eq(SysUser::getPhonenumber, user.getPhonenumber()).last("limit 1")); + if (Validator.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验email是否唯一 + * + * @param user 用户信息 + * @return + */ + @Override + public String checkEmailUnique(SysUser user) { + Long userId = Validator.isNull(user.getUserId()) ? -1L : user.getUserId(); + SysUser info = getOne(new LambdaQueryWrapper() + .select(SysUser::getUserId, SysUser::getEmail) + .eq(SysUser::getEmail, user.getEmail()).last("limit 1")); + if (Validator.isNotNull(info) && info.getUserId().longValue() != userId.longValue()) { + return UserConstants.NOT_UNIQUE; + } + return UserConstants.UNIQUE; + } + + /** + * 校验用户是否允许操作 + * + * @param user 用户信息 + */ + @Override + public void checkUserAllowed(SysUser user) { + if (Validator.isNotNull(user.getUserId()) && user.isAdmin()) { + throw new CustomException("不允许操作超级管理员用户"); + } + } + + /** + * 新增保存用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + @Transactional + public int insertUser(SysUser user) { + // 新增用户信息 + int rows = baseMapper.insert(user); + // 新增用户岗位关联 + insertUserPost(user); + // 新增用户与角色管理 + insertUserRole(user); + return rows; + } + + /** + * 修改保存用户信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + @Transactional + public int updateUser(SysUser user) { + Long userId = user.getUserId(); + // 删除用户与角色关联 + userRoleMapper.delete(new LambdaQueryWrapper().eq(SysUserRole::getUserId,userId)); + // 新增用户与角色管理 + insertUserRole(user); + // 删除用户与岗位关联 + userPostMapper.delete(new LambdaQueryWrapper().eq(SysUserPost::getUserId,userId)); + // 新增用户与岗位管理 + insertUserPost(user); + return baseMapper.updateById(user); + } + + /** + * 修改用户状态 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public int updateUserStatus(SysUser user) { + return baseMapper.updateById(user); + } + + /** + * 修改用户基本信息 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public int updateUserProfile(SysUser user) { + return baseMapper.updateById(user); + } + + /** + * 修改用户头像 + * + * @param userName 用户名 + * @param avatar 头像地址 + * @return 结果 + */ + @Override + public boolean updateUserAvatar(String userName, String avatar) { + return baseMapper.update(null, + new LambdaUpdateWrapper() + .set(SysUser::getAvatar,avatar) + .eq(SysUser::getUserName,userName)) > 0; + } + + /** + * 重置用户密码 + * + * @param user 用户信息 + * @return 结果 + */ + @Override + public int resetPwd(SysUser user) { + return baseMapper.updateById(user); + } + + /** + * 重置用户密码 + * + * @param userName 用户名 + * @param password 密码 + * @return 结果 + */ + @Override + public int resetUserPwd(String userName, String password) { + return baseMapper.update(null, + new LambdaUpdateWrapper() + .set(SysUser::getPassword,password) + .eq(SysUser::getUserName,userName)); + } + + /** + * 新增用户角色信息 + * + * @param user 用户对象 + */ + public void insertUserRole(SysUser user) { + Long[] roles = user.getRoleIds(); + if (Validator.isNotNull(roles)) { + // 新增用户与角色管理 + List list = new ArrayList(); + for (Long roleId : roles) { + SysUserRole ur = new SysUserRole(); + ur.setUserId(user.getUserId()); + ur.setRoleId(roleId); + list.add(ur); + } + if (list.size() > 0) { + userRoleMapper.insertAll(list); + } + } + } + + /** + * 新增用户岗位信息 + * + * @param user 用户对象 + */ + public void insertUserPost(SysUser user) { + Long[] posts = user.getPostIds(); + if (Validator.isNotNull(posts)) { + // 新增用户与岗位管理 + List list = new ArrayList(); + for (Long postId : posts) { + SysUserPost up = new SysUserPost(); + up.setUserId(user.getUserId()); + up.setPostId(postId); + list.add(up); + } + if (list.size() > 0) { + userPostMapper.insertAll(list); + } + } + } + + /** + * 通过用户ID删除用户 + * + * @param userId 用户ID + * @return 结果 + */ + @Override + @Transactional + public int deleteUserById(Long userId) { + // 删除用户与角色关联 + userRoleMapper.delete(new LambdaQueryWrapper().eq(SysUserRole::getUserId,userId)); + // 删除用户与岗位表 + userPostMapper.delete(new LambdaQueryWrapper().eq(SysUserPost::getUserId,userId)); + return baseMapper.deleteById(userId); + } + + /** + * 批量删除用户信息 + * + * @param userIds 需要删除的用户ID + * @return 结果 + */ + @Override + @Transactional + public int deleteUserByIds(Long[] userIds) { + for (Long userId : userIds) { + checkUserAllowed(new SysUser(userId)); + } + List ids = Arrays.asList(userIds); + // 删除用户与角色关联 + userRoleMapper.delete(new LambdaQueryWrapper().in(SysUserRole::getUserId,ids)); + // 删除用户与岗位表 + userPostMapper.delete(new LambdaQueryWrapper().in(SysUserPost::getUserId,ids)); + return baseMapper.deleteBatchIds(ids); + } + + /** + * 导入用户数据 + * + * @param userList 用户数据列表 + * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据 + * @param operName 操作用户 + * @return 结果 + */ + @Override + public String importUser(List userList, Boolean isUpdateSupport, String operName) { + if (Validator.isNull(userList) || userList.size() == 0) { + throw new CustomException("导入用户数据不能为空!"); + } + int successNum = 0; + int failureNum = 0; + StringBuilder successMsg = new StringBuilder(); + StringBuilder failureMsg = new StringBuilder(); + String password = configService.selectConfigByKey("sys.user.initPassword"); + for (SysUser user : userList) { + try { + // 验证是否存在这个用户 + SysUser u = baseMapper.selectUserByUserName(user.getUserName()); + if (Validator.isNull(u)) { + user.setPassword(SecurityUtils.encryptPassword(password)); + user.setCreateBy(operName); + this.insertUser(user); + successNum++; + successMsg.append("
" + successNum + "、账号 " + user.getUserName() + " 导入成功"); + } else if (isUpdateSupport) { + user.setUpdateBy(operName); + this.updateUser(user); + successNum++; + successMsg.append("
" + successNum + "、账号 " + user.getUserName() + " 更新成功"); + } else { + failureNum++; + failureMsg.append("
" + failureNum + "、账号 " + user.getUserName() + " 已存在"); + } + } catch (Exception e) { + failureNum++; + String msg = "
" + failureNum + "、账号 " + user.getUserName() + " 导入失败:"; + failureMsg.append(msg + e.getMessage()); + log.error(msg, e); + } + } + if (failureNum > 0) { + failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:"); + throw new CustomException(failureMsg.toString()); + } else { + successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:"); + } + return successMsg.toString(); + } + + @Override + public SysUser selectUserByMobile(String mobile) { + return baseMapper.selectUserByMobile(mobile); + } + + @Override + public SysUser selectUserByOpenId(String openId) { + return baseMapper.selectUserByOpenId(openId); + } + +} diff --git a/bashi-system/src/main/resources/mapper/system/SysConfigMapper.xml b/bashi-system/src/main/resources/mapper/system/SysConfigMapper.xml new file mode 100644 index 0000000..b0fd545 --- /dev/null +++ b/bashi-system/src/main/resources/mapper/system/SysConfigMapper.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + diff --git a/bashi-system/src/main/resources/mapper/system/SysDeptMapper.xml b/bashi-system/src/main/resources/mapper/system/SysDeptMapper.xml new file mode 100644 index 0000000..dfbdf17 --- /dev/null +++ b/bashi-system/src/main/resources/mapper/system/SysDeptMapper.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time + from sys_dept d + + + + + + + + update sys_dept set ancestors = + + when #{item.deptId} then #{item.ancestors} + + where dept_id in + + #{item.deptId} + + + + diff --git a/bashi-system/src/main/resources/mapper/system/SysDictDataMapper.xml b/bashi-system/src/main/resources/mapper/system/SysDictDataMapper.xml new file mode 100644 index 0000000..c41b4f7 --- /dev/null +++ b/bashi-system/src/main/resources/mapper/system/SysDictDataMapper.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/bashi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml b/bashi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml new file mode 100644 index 0000000..432c805 --- /dev/null +++ b/bashi-system/src/main/resources/mapper/system/SysDictTypeMapper.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/bashi-system/src/main/resources/mapper/system/SysLogininforMapper.xml b/bashi-system/src/main/resources/mapper/system/SysLogininforMapper.xml new file mode 100644 index 0000000..9e8ddd7 --- /dev/null +++ b/bashi-system/src/main/resources/mapper/system/SysLogininforMapper.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + diff --git a/bashi-system/src/main/resources/mapper/system/SysMenuMapper.xml b/bashi-system/src/main/resources/mapper/system/SysMenuMapper.xml new file mode 100644 index 0000000..5354270 --- /dev/null +++ b/bashi-system/src/main/resources/mapper/system/SysMenuMapper.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + select menu_id, + menu_name, + parent_id, + order_num, + path, + component, + is_frame, + is_cache, + menu_type, + visible, + status, + ifnull(perms, '') as perms, + icon, + create_time + from sys_menu + + + + + + + + + + + + + + + + diff --git a/bashi-system/src/main/resources/mapper/system/SysNoticeMapper.xml b/bashi-system/src/main/resources/mapper/system/SysNoticeMapper.xml new file mode 100644 index 0000000..0e71033 --- /dev/null +++ b/bashi-system/src/main/resources/mapper/system/SysNoticeMapper.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + diff --git a/bashi-system/src/main/resources/mapper/system/SysOperLogMapper.xml b/bashi-system/src/main/resources/mapper/system/SysOperLogMapper.xml new file mode 100644 index 0000000..17f205b --- /dev/null +++ b/bashi-system/src/main/resources/mapper/system/SysOperLogMapper.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bashi-system/src/main/resources/mapper/system/SysPostMapper.xml b/bashi-system/src/main/resources/mapper/system/SysPostMapper.xml new file mode 100644 index 0000000..0bff5fe --- /dev/null +++ b/bashi-system/src/main/resources/mapper/system/SysPostMapper.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + select post_id, + post_code, + post_name, + post_sort, + status, + create_by, + create_time, + remark + from sys_post + + + + + + + diff --git a/bashi-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml b/bashi-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml new file mode 100644 index 0000000..62cbe81 --- /dev/null +++ b/bashi-system/src/main/resources/mapper/system/SysRoleDeptMapper.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/bashi-system/src/main/resources/mapper/system/SysRoleMapper.xml b/bashi-system/src/main/resources/mapper/system/SysRoleMapper.xml new file mode 100644 index 0000000..05ef675 --- /dev/null +++ b/bashi-system/src/main/resources/mapper/system/SysRoleMapper.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + select distinct r.role_id, + r.role_name, + r.role_key, + r.role_sort, + r.data_scope, + r.menu_check_strictly, + r.dept_check_strictly, + r.status, + r.del_flag, + r.create_time, + r.remark + from sys_role r + left join sys_user_role ur on ur.role_id = r.role_id + left join sys_user u on u.user_id = ur.user_id + left join sys_dept d on u.dept_id = d.dept_id + + + + + + + + + + + + + diff --git a/bashi-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml b/bashi-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml new file mode 100644 index 0000000..1a72add --- /dev/null +++ b/bashi-system/src/main/resources/mapper/system/SysRoleMenuMapper.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/bashi-system/src/main/resources/mapper/system/SysUserMapper.xml b/bashi-system/src/main/resources/mapper/system/SysUserMapper.xml new file mode 100644 index 0000000..3ac99bc --- /dev/null +++ b/bashi-system/src/main/resources/mapper/system/SysUserMapper.xml @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select u.user_id, + u.dept_id, + u.user_name, + u.nick_name, + u.email, + u.avatar, + u.phonenumber, + u.password, + u.sex, + u.status, + u.del_flag, + u.login_ip, + u.login_date, + u.create_by, + u.create_time, + u.remark, + d.dept_id, + d.parent_id, + d.dept_name, + d.order_num, + d.leader, + d.status as dept_status, + r.role_id, + r.role_name, + r.role_key, + r.role_sort, + r.data_scope, + r.status as role_status + from sys_user u + left join sys_dept d on u.dept_id = d.dept_id + left join sys_user_role ur on u.user_id = ur.user_id + left join sys_role r on r.role_id = ur.role_id + + + + + + + + + + + + + + + diff --git a/bashi-system/src/main/resources/mapper/system/SysUserPostMapper.xml b/bashi-system/src/main/resources/mapper/system/SysUserPostMapper.xml new file mode 100644 index 0000000..fb130c5 --- /dev/null +++ b/bashi-system/src/main/resources/mapper/system/SysUserPostMapper.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/bashi-system/src/main/resources/mapper/system/SysUserRoleMapper.xml b/bashi-system/src/main/resources/mapper/system/SysUserRoleMapper.xml new file mode 100644 index 0000000..f2ba2b7 --- /dev/null +++ b/bashi-system/src/main/resources/mapper/system/SysUserRoleMapper.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/hd_bg.png b/hd_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..7e9e92db391989e6cd6262889ef0d5a68d07ca94 GIT binary patch literal 167874 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 literal 0 HcmV?d00001 diff --git a/hd_bg1.png b/hd_bg1.png new file mode 100644 index 0000000000000000000000000000000000000000..d859058a64c4de2730f1b8684c03a091b6e20f68 GIT binary patch literal 110000 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 z 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 z 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 QR{#J207*qoM6N<$f)|J68vp + + 4.0.0 + + com.bashi + bashi + 2.4.0 + + bashi + + + 2.4.0 + 2.4.7 + UTF-8 + UTF-8 + 1.8 + 3.1.1 + 1.2.6 + 3.0.2 + 4.1.2 + 1.7 + 0.9.1 + 3.4.3.4 + 5.8.22 + 3.0.3 + 11.0 + 2.4.1 + 3.15.2 + 2.2.1 + 3.4.0 + 1.2.79 + + + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + com.alibaba + druid-spring-boot-starter + ${druid.version} + + + + com.github.xiaoymin + knife4j-spring-boot-starter + ${knife4j.version} + + + + + org.apache.poi + poi-ooxml + ${poi.version} + + + + + org.apache.velocity + velocity + ${velocity.version} + + + + + io.jsonwebtoken + jjwt + ${jwt.version} + + + + + com.baomidou + dynamic-datasource-spring-boot-starter + ${datasource.version} + + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + com.baomidou + mybatis-plus-extension + ${mybatis-plus.version} + + + + cn.hutool + hutool-all + ${hutool.version} + + + + org.springframework.cloud + spring-cloud-starter-openfeign + ${feign.version} + + + + io.github.openfeign + feign-okhttp + ${feign-okhttp.version} + + + + de.codecentric + spring-boot-admin-starter-server + ${spring-boot-admin.version} + + + de.codecentric + spring-boot-admin-starter-client + ${spring-boot-admin.version} + + + + + org.redisson + redisson-spring-boot-starter + ${redisson.version} + + + com.baomidou + lock4j-redisson-spring-boot-starter + ${lock4j.version} + + + + com.bashi + bashi-quartz + 2.4.0 + + + + + com.bashi + bashi-generator + 2.4.0 + + + + + com.bashi + bashi-framework + 2.4.0 + + + + + com.bashi + bashi-system + 2.4.0 + + + + + com.bashi + bashi-common + 2.4.0 + + + + + com.alibaba + fastjson + ${fastjson.version} + + + + + + + bashi-admin + bashi-framework + bashi-system + bashi-quartz + bashi-generator + bashi-common + bashi-dk + + pom + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + ${java.version} + ${java.version} + ${project.build.sourceEncoding} + + + + + + + src/main/resources + + true + + + + + + + public + aliyun nexus + http://maven.aliyun.com/nexus/content/groups/public/ + + true + + + + + + + public + aliyun nexus + http://maven.aliyun.com/nexus/content/groups/public/ + + true + + + false + + + + + + + local + + + local + debug + + + + dev + + + dev + debug + + + + true + + + + prod + + prod + info + + + + + + jdk8 + + true + 1.8 + + + 1.8 + + + + + diff --git a/sql/bus.sql b/sql/bus.sql new file mode 100644 index 0000000..35b9731 --- /dev/null +++ b/sql/bus.sql @@ -0,0 +1,351 @@ +CREATE TABLE `bus_order_count` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT comment 'id', + `count_date` date not NULL comment '订单金额', + `order_amount` bigint(11) not NULL DEFAULT 0 comment '订单金额', + `vip_amount` bigint(11) not NULL DEFAULT 0 comment '会员金额', + `qualification_amount` bigint(11) not NULL DEFAULT 0 comment '证书办理金额', + `order_num` bigint(11) not null DEFAULT 0 comment '订单销售量', + `vip_num` bigint(11) not null DEFAULT 0 comment 'vip销售量', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC comment '订单统计接口'; + +CREATE TABLE `bus_shop_amount_logs` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT comment 'id', + `shop_id` bigint(20) NOT NULL comment '商家ID', + `incs_amount` bigint(20) NOT NULL comment '修改金额', + `operator_id` bigint(20) NOT NULL comment '操作人ID', + `operator_name` varchar(200) NOT NULL comment '操作人', + `operator_remark` varchar(255) comment '操作说明', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC comment '商家修改初始化金额操作记录'; + + +CREATE TABLE `bus_order_logs` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT comment 'id', + `order_id` bigint(20) NOT NULL comment '订单id', + `user_id` bigint(20) NOT NULL comment '用户id', + `user_name` varchar(200) NOT NULL comment '操作人', + `operator_step` varchar(255) comment '操作说明', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC comment '订单操作说明'; + +CREATE TABLE `bus_system_config` ( + `id` bigint(20) NOT NULL, + `vip_auth` bit(1) DEFAULT 1 comment '是否开启Vip权限-关闭vip权限,则视频和图像可直接观看', + `default_order_commit` int(2) DEFAULT 10 COMMENT '默认的平台抽佣比例 1-100', + `platform_phone` varchar(36) comment '平台电话', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC comment '系统设置'; + +CREATE TABLE `bus_evaluation` ( + `id` bigint(20) NOT NULL, + `order_id` bigint(20) DEFAULT NULL comment '订单id', + `order_no` varchar(64) DEFAULT NULL comment '订单编号', + `service_item_id` bigint(20) default null comment '服务ID', + `content` varchar(500) DEFAULT NULL comment '评价内容-不超过500字符', + `status` int(11) NOT NULL DEFAULT 1 COMMENT '状态 0-停用 1-启用', + `customer_id` bigint(20) DEFAULT NULL COMMENT '客户ID', + `customer_avatar` varchar(255) default null comment '客户头像', + `customer_nick_name` varchar(255) default null comment '客户昵称', + `shop_id` bigint(20) DEFAULT NULL comment '商家ID', + `mechanic_id` bigint(20) DEFAULT NULL comment '技师ID', + `mechanic_nick_name` bigint(20) DEFAULT NULL comment '技师昵称', + `score` int(1) DEFAULT NULL comment '分数 1-5', + `create_time` datetime not null default now(), + `virtual` bit(1) not NULL default 0 comment '是否为虚拟评价', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC comment '评价页面'; + +CREATE TABLE `bus_coupon` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT comment 'id', + `name` varchar(255) DEFAULT NULL comment '优惠券名称', + `image` varchar(1024) DEFAULT NULL comment '优惠券图片', + `note` varchar(255) DEFAULT NULL comment '优惠券说明', + `value` bigint(11) not null comment '优惠券金额', + `use_condition` bigint(20) not null comment '优惠券使用条件', + `status` int(2) not null default 1 comment '状态 0-停用 1-启用', + `time_start` date not null comment '优惠券生效时间', + `time_end` date not null comment '优惠券结束时间', + `count` int(11) not null comment '优惠券发放数量', + `got_count` int(11) not NULL comment '优惠券已领数量', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC comment '优惠券中心'; + +CREATE TABLE `bus_customer` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT comment '客户id', + `user_id` bigint(20) NOT NULL comment '用户id', + `vip_end` date DEFAULT NULL comment 'vip到期时间', + `vip` bit(1) default 0 comment '是否为vip', + `avatar` varchar(255) comment '客户头像', + `nick_name` varchar(255) comment '客户昵称', + `phone` varchar(13) comment '客户电话', + `sex` tinyint(2) DEFAULT NULL comment '性别 0 男 1 女', + `merchant` bit(1) default 0 comment '是否商家入驻', + `mechanic` bit(1) default 0 comment '是否技师入驻', + PRIMARY KEY (`id`), + KEY `user_idx` (`user_id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC comment '客户管理'; + + +CREATE TABLE `bus_customer_star` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT comment 'id', + `user_id` bigint(20) NOT NULL comment '客户id', + `star_mechanic_id` bigint(20) NOT NULL comment '关注的技师ID', + PRIMARY KEY (`id`), + KEY `user_idx` (`user_id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC comment '客户关注技师表'; + +CREATE TABLE `bus_customer_address` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT comment 'id', + `user_id` bigint(20) NOT NULL comment '用户id', + `customer_id` bigint(20) NOT NULL comment '客户id', + `address` varchar(1024) DEFAULT NULL comment '地址', + `house_number` varchar(64) DEFAULT NULL comment '门牌号', + `longitude` decimal(30,8) DEFAULT NULL COMMENT '经度', + `latitude` decimal(30,8) DEFAULT NULL COMMENT '纬度', + `name` varchar(255) NOT NULL comment '用户名称', + `phone` varchar(64) NOT NULL comment '用户手机', + `be_default` bit(1) NOT NULL default 0 comment '是否默认地址 0-不默认 1-默认', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC comment '客户地址管理'; + +CREATE TABLE `bus_customer_coupon` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT comment '客户id', + `name` varchar(255) DEFAULT NULL comment '优惠券名称', + `image` varchar(1024) DEFAULT NULL comment '优惠券图片', + `note` varchar(255) DEFAULT NULL comment '优惠券说明', + `value` bigint(11) not null comment '优惠券金额', + `use_condition` bigint(20) not null comment '优惠券使用条件', + `time_start` date not null comment '优惠券生效时间', + `time_end` date not null comment '优惠券结束时间', + `used` bit(1) DEFAULT 0 not null comment '是否使用', + `user_id` bigint(20) not NULL comment '用户id', + `sys_coupon_id` bigint(20) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `user_idx` (`user_id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC comment '客户优惠券'; + + +CREATE TABLE `bus_mechanic` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT comment '技师id', + `user_id` bigint(20) NOT NULL comment '用户id', + `income` bigint(20) NOT NULL DEFAULT 0 comment '收入', + `balance` bigint(20) NOT NULL DEFAULT 0 comment '余额', + `work_time_start` varchar(64) comment '开始接单时间', + `work_time_end` varchar(64) comment '结束接单时间', + `qualification` varchar(2048) comment '资格证书图片', + `identity_body` varchar(2048) comment '实名信息', + `status` int(2) DEFAULT NULL comment '状态 0-未审核 1-审核通过 3-暂停服务', + `service_items` varchar(255) DEFAULT NULL, + `service_year` int(3) comment '服务年限', + `phone_auth` bit(1) default 1 comment '手机认证', + `avatar_auth` bit(1) default 1 comment '头像认证', + `identity_auth` bit(1) default 1 comment '实名认证', + `health_auth` bit(1) default 1 comment '健康档案', + `qua_auth` bit(1) default 1 comment '资质档案', + `shop_id` bigint(20) comment '所属店铺', + `similarity` int(3) comment '头像相识度', + `order_count` int(11) not null default 0 comment '接单数量', + `applause_rate` int(11) not null default 100 comment '好评率', + `sex` int(2) comment '性别 0 男 1 女', + `avatar` varchar(255) comment '技师头像', + `real_avatar` varchar(255) comment '真实头像', + `read_name` varchar(64) comment '技师真实名称', + `nick_name` varchar(64) comment '技师昵称', + `address` varchar(255) DEFAULT NULL comment '技师地址', + `technical_title` varchar(1024) DEFAULT NULL, + `favorite_count` int(11) comment '关注数量' DEFAULT NULL, + `service_status` int(2) NOT NULL default 1 comment '接单状态 0 下线 1 在线', + `phone` varchar(64) NOT NULL comment '技师手机号码', + `remark` varchar(512) DEFAULT NULL comment '自我介绍', + `city` varchar(255) DEFAULT NULL, + `sign_time` datetime default null comment '签约时间', + `join_time` datetime default null comment '注册时间', + `app_type` varchar(10) DEFAULT NULL comment '所属APP', + `red_card` bit(1) DEFAULT b'0' COMMENT '红牌技师', + `longitude` decimal(30,8) DEFAULT NULL COMMENT '经度', + `latitude` decimal(30,8) DEFAULT NULL COMMENT '纬度', + `audit_avatar` varchar(255) DEFAULT NULL COMMENT '待审核的头像 - 空表示没有', + `audit_avatar_status` int(2) DEFAULT NULL COMMENT '头像审核状态 - 0-无审核 1-审核中 -1-审核失败', + `audit_avatar_message` varchar(255) DEFAULT NULL COMMENT '头像原因', + `limit_distance` int(5) NOT NULL DEFAULT '0' COMMENT '最大接单距离', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC comment '技师管理'; + +CREATE TABLE `bus_mechanic_info` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT comment 'id', + `user_id` bigint(20) NOT NULL comment '用户id', + `mechanic_id` bigint(20) NOT NULL DEFAULT 0 comment '技师id', + `info_type` int(2) NOT NULL comment '1-照片 2-视频', + `info_url` varchar(125) comment '内容', + `status` int(2) not null comment '状态 0-正常 1-下架', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC comment '技师照片-视频管理'; + +CREATE TABLE `bus_shop` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) NOT NULL comment '用户id', + + `shop_avatar` varchar(1024) DEFAULT NULL comment '店铺头像', + `nick_name` varchar(255) DEFAULT NULL comment '企业昵称', + `id_front_url` varchar(255) DEFAULT NULL comment '身份证正面', + `id_back_url` varchar(255) DEFAULT NULL comment '身份证背面', + `id_hand_url` varchar(255) DEFAULT NULL comment '手持身份证照片', + `license_url` varchar(255) DEFAULT NULL comment '营业执照图片', + `capital` varchar(255) DEFAULT NULL comment '注册资本', + `shop_name` varchar(255) DEFAULT NULL comment '企业名称', + `address` varchar(512) DEFAULT NULL comment '店铺地址', + `shop_code` varchar(128) DEFAULT NULL comment '统一社会信用代码', + `invite_code` varchar(16) DEFAULT NULL comment '邀请码', + `introduction` varchar(500) COMMENT '店铺简介(不超过500字符)', + `legal_person` varchar(50) comment '店铺法人', + + `mechanic_count` int(11) DEFAULT 0 comment '技师数量', + `order_count` int(11) DEFAULT 0 comment '订单数量', + `amount` bigint(20) DEFAULT 0 comment '余额', + `total_income` bigint(20) DEFAULT 0 comment '累计收入', + `register_date` datetime default now() comment '注册时间', + `in_time` datetime comment '审核时间', + `commission_rate` int(3) not null default 10 comment '平台提成', + + + `stop_status` int(2) default 0 not null comment '暂停状态', + `audit_status` int(2) default 0 not null comment '审核状态 0-待审核 1-审核通过 -1-审核失败', + + `wx_open_id` varchar(64) DEFAULT NULL comment 'wxOpenId', + `wx_nick_name` varchar(64) DEFAULT NULL comment '微信昵称', + `wx_avatar` varchar(64) DEFAULT NULL comment '微信头像', + `bank_card_no` varchar(128) DEFAULT NULL comment '银行卡号', + `bank_card_name` varchar(128) DEFAULT NULL comment '银行卡姓名', + `bank` varchar(512) DEFAULT NULL comment '银行名称', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC comment '商家管理'; + +CREATE TABLE `bus_service_item` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `name` varchar(255) DEFAULT NULL comment '服务名称', + `service_time` int(11) DEFAULT NULL comment '服务时间', + `offline_price` bigint(20) DEFAULT NULL comment '线下价格', + `online_price` bigint(10) DEFAULT NULL comment '线上价格', + `level` int(11) DEFAULT NULL comment '服务星级(0,1,2,3,4,5)', + `image` varchar(2048) DEFAULT NULL comment '项目头像', + `status` int(2) default 0 not null comment '是否上线(0下线 1上线)', +# `part` varchar(2048) comment '', + `shop_id` bigint(20) comment '所属店铺', + `order_count` int(11) DEFAULT 0 comment '下单数量', + `audit_status` int(2) NOT NULL DEFAULT '0' COMMENT '审核状态 0-待审核 1-审核通过 -1-审核失败', + `audit_message` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC comment '服务管理'; + +CREATE TABLE `bus_item_assign` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `mechanic_id` bigint(20) DEFAULT NULL, + `item_id` bigint(11) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC comment '技师-服务'; + +CREATE TABLE `bus_banner` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `banner_url` varchar(255) DEFAULT NULL comment 'banner图', + `status` int(2) default 1 not null comment '状态 0-停用 1-启用', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC comment 'banner图'; + +CREATE TABLE `bus_order` ( + `id` bigint(20) NOT NULL, + `trade_no` varchar(128) DEFAULT NULL, + + `customer_id` bigint(20) NOT NULL comment '客户ID', + `customer_phone` varchar(64) DEFAULT NULL comment '客户预约手机', + `customer_name` varchar(64) DEFAULT NULL comment '客户预约名称', + `customer_address` varchar(1024) DEFAULT NULL comment '客户地址', + `customer_address_c` varchar(1024) DEFAULT NULL comment '客户坐标', + + `mechanic_id` bigint(20) NOT NULL comment '技师id', + `mechanic_address` varchar(1024) DEFAULT NULL comment '技师地址', + `mechanic_address_c` varchar(1024) DEFAULT NULL comment '技师坐标', + `mechanic_avatar` varchar(255) comment '技师头像', + `mechanic_nick_name` varchar(255) comment '技师昵称', + + `shop_id` bigint(11) NOT NULL comment '商家id', + `service_item_id` bigint(20) DEFAULT NULL comment '服务id', + `coupon_id` bigint(20) default null comment '优惠券id', + `coupon` bigint(20) default null comment '优惠券抵扣', + + + `fare` bigint(20) default 0 comment '车费', + `price` bigint(20) default 0 NOT NULL comment '商品价格', + `amount` bigint(20) default 0 not NULL comment '最终实付金额', + `boss_amount` int(11) DEFAULT 0 comment '平台收入', + `commission_rate` int(3) DEFAULT 0 comment '平台利润', + + `notes` varchar(255) DEFAULT NULL comment '备注', + + `pay_status` int(11) not null default 0 comment '支付状态 0-未支付 1-已支付 2-已退款', + `service_status` int(11) not NULL default 0 comment '服务状态 0-未接单 1-已接单 2-已完成', + `pay_type` int(11) DEFAULT NULL comment '支付类型 1-支付宝 2-微信', + `appraise` bit(1) not NULL default 0 comment '是否评价', + + `create_time` datetime not NULL default now() comment '创建时间', + `pay_time` datetime comment '支付时间', + `order_time` datetime comment '预约时间', + `service_time` datetime comment '接单时间', + `complete_time` datetime comment '完成时间', + `distance` varchar(255) comment '距离', + + `agent` varchar(10) DEFAULT NULL, + `delete_flag` int(2) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC comment '订单管理'; + +CREATE TABLE `bus_order` ( + `id` bigint(20) NOT NULL, + `trade_no` varchar(128) DEFAULT NULL, + + `customer_id` bigint(20) NOT NULL comment '客户ID', + `customer_phone` varchar(64) DEFAULT NULL comment '客户预约手机', + `customer_name` varchar(64) DEFAULT NULL comment '客户预约名称', + `customer_address` varchar(1024) DEFAULT NULL comment '客户地址', + `customer_address_c` varchar(1024) DEFAULT NULL comment '客户坐标', + + `mechanic_id` bigint(20) NOT NULL comment '技师id', + `mechanic_address` varchar(1024) DEFAULT NULL comment '技师地址', + `mechanic_address_c` varchar(1024) DEFAULT NULL comment '技师坐标', + `mechanic_avatar` varchar(255) comment '技师头像', + `mechanic_nick_name` varchar(255) comment '技师昵称', + + `shop_id` bigint(11) NOT NULL comment '商家id', + `service_item_id` bigint(20) DEFAULT NULL comment '服务id', + `coupon_id` bigint(20) default null comment '优惠券id', + `coupon` bigint(20) default null comment '优惠券抵扣', + + + `fare` bigint(20) default 0 comment '车费', + `price` bigint(20) default 0 NOT NULL comment '商品价格', + `amount` bigint(20) default 0 not NULL comment '最终实付金额', + `boss_amount` int(11) DEFAULT 0 comment '平台收入', + `commission_rate` int(3) DEFAULT 0 comment '平台利润', + + `notes` varchar(255) DEFAULT NULL comment '备注', + + `pay_status` int(11) not null default 0 comment '支付状态 0-未支付 1-已支付 2-已退款', + `service_status` int(11) not NULL default 0 comment '服务状态 0-未接单 1-已接单 2-已完成', + `pay_type` int(11) DEFAULT NULL comment '支付类型 1-支付宝 2-微信', + `appraise` bit(1) not NULL default 0 comment '是否评价', + + `create_time` datetime not NULL default now() comment '创建时间', + `pay_time` datetime comment '支付时间', + `order_time` datetime comment '预约时间', + `service_time` datetime comment '接单时间', + `complete_time` datetime comment '完成时间', + `distance` int(20) comment '距离-单位米', + + `agent` varchar(10) DEFAULT NULL, + `delete_flag` int(2) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC comment '订单管理'; + + + + diff --git a/sql/dk.sql b/sql/dk.sql new file mode 100644 index 0000000..6b7bdf2 --- /dev/null +++ b/sql/dk.sql @@ -0,0 +1,2 @@ +ALTER TABLE `dk`.`sys_user` + ADD COLUMN `open_id` varchar(255) NULL COMMENT 'open-id' AFTER `remark`; diff --git a/sql/quartz.sql b/sql/quartz.sql new file mode 100644 index 0000000..55665e2 --- /dev/null +++ b/sql/quartz.sql @@ -0,0 +1,170 @@ +-- ---------------------------- +-- 1、存储每一个已配置的 jobDetail 的详细信息 +-- ---------------------------- +drop table if exists QRTZ_JOB_DETAILS; +create table QRTZ_JOB_DETAILS ( + sched_name varchar(120) not null, + job_name varchar(200) not null, + job_group varchar(200) not null, + description varchar(250) null, + job_class_name varchar(250) not null, + is_durable varchar(1) not null, + is_nonconcurrent varchar(1) not null, + is_update_data varchar(1) not null, + requests_recovery varchar(1) not null, + job_data blob null, + primary key (sched_name,job_name,job_group) +) engine=innodb; + +-- ---------------------------- +-- 2、 存储已配置的 Trigger 的信息 +-- ---------------------------- +drop table if exists QRTZ_TRIGGERS; +create table QRTZ_TRIGGERS ( + sched_name varchar(120) not null, + trigger_name varchar(200) not null, + trigger_group varchar(200) not null, + job_name varchar(200) not null, + job_group varchar(200) not null, + description varchar(250) null, + next_fire_time bigint(13) null, + prev_fire_time bigint(13) null, + priority integer null, + trigger_state varchar(16) not null, + trigger_type varchar(8) not null, + start_time bigint(13) not null, + end_time bigint(13) null, + calendar_name varchar(200) null, + misfire_instr smallint(2) null, + job_data blob null, + primary key (sched_name,trigger_name,trigger_group), + foreign key (sched_name,job_name,job_group) references QRTZ_JOB_DETAILS(sched_name,job_name,job_group) +) engine=innodb; + +-- ---------------------------- +-- 3、 存储简单的 Trigger,包括重复次数,间隔,以及已触发的次数 +-- ---------------------------- +drop table if exists QRTZ_SIMPLE_TRIGGERS; +create table QRTZ_SIMPLE_TRIGGERS ( + sched_name varchar(120) not null, + trigger_name varchar(200) not null, + trigger_group varchar(200) not null, + repeat_count bigint(7) not null, + repeat_interval bigint(12) not null, + times_triggered bigint(10) not null, + primary key (sched_name,trigger_name,trigger_group), + foreign key (sched_name,trigger_name,trigger_group) references QRTZ_TRIGGERS(sched_name,trigger_name,trigger_group) +) engine=innodb; + +-- ---------------------------- +-- 4、 存储 Cron Trigger,包括 Cron 表达式和时区信息 +-- ---------------------------- +drop table if exists QRTZ_CRON_TRIGGERS; +create table QRTZ_CRON_TRIGGERS ( + sched_name varchar(120) not null, + trigger_name varchar(200) not null, + trigger_group varchar(200) not null, + cron_expression varchar(200) not null, + time_zone_id varchar(80), + primary key (sched_name,trigger_name,trigger_group), + foreign key (sched_name,trigger_name,trigger_group) references QRTZ_TRIGGERS(sched_name,trigger_name,trigger_group) +) engine=innodb; + +-- ---------------------------- +-- 5、 Trigger 作为 Blob 类型存储(用于 Quartz 用户用 JDBC 创建他们自己定制的 Trigger 类型,JobStore 并不知道如何存储实例的时候) +-- ---------------------------- +drop table if exists QRTZ_BLOB_TRIGGERS; +create table QRTZ_BLOB_TRIGGERS ( + sched_name varchar(120) not null, + trigger_name varchar(200) not null, + trigger_group varchar(200) not null, + blob_data blob null, + primary key (sched_name,trigger_name,trigger_group), + foreign key (sched_name,trigger_name,trigger_group) references QRTZ_TRIGGERS(sched_name,trigger_name,trigger_group) +) engine=innodb; + +-- ---------------------------- +-- 6、 以 Blob 类型存储存放日历信息, quartz可配置一个日历来指定一个时间范围 +-- ---------------------------- +drop table if exists QRTZ_CALENDARS; +create table QRTZ_CALENDARS ( + sched_name varchar(120) not null, + calendar_name varchar(200) not null, + calendar blob not null, + primary key (sched_name,calendar_name) +) engine=innodb; + +-- ---------------------------- +-- 7、 存储已暂停的 Trigger 组的信息 +-- ---------------------------- +drop table if exists QRTZ_PAUSED_TRIGGER_GRPS; +create table QRTZ_PAUSED_TRIGGER_GRPS ( + sched_name varchar(120) not null, + trigger_group varchar(200) not null, + primary key (sched_name,trigger_group) +) engine=innodb; + +-- ---------------------------- +-- 8、 存储与已触发的 Trigger 相关的状态信息,以及相联 Job 的执行信息 +-- ---------------------------- +drop table if exists QRTZ_FIRED_TRIGGERS; +create table QRTZ_FIRED_TRIGGERS ( + sched_name varchar(120) not null, + entry_id varchar(95) not null, + trigger_name varchar(200) not null, + trigger_group varchar(200) not null, + instance_name varchar(200) not null, + fired_time bigint(13) not null, + sched_time bigint(13) not null, + priority integer not null, + state varchar(16) not null, + job_name varchar(200) null, + job_group varchar(200) null, + is_nonconcurrent varchar(1) null, + requests_recovery varchar(1) null, + primary key (sched_name,entry_id) +) engine=innodb; + +-- ---------------------------- +-- 9、 存储少量的有关 Scheduler 的状态信息,假如是用于集群中,可以看到其他的 Scheduler 实例 +-- ---------------------------- +drop table if exists QRTZ_SCHEDULER_STATE; +create table QRTZ_SCHEDULER_STATE ( + sched_name varchar(120) not null, + instance_name varchar(200) not null, + last_checkin_time bigint(13) not null, + checkin_interval bigint(13) not null, + primary key (sched_name,instance_name) +) engine=innodb; + +-- ---------------------------- +-- 10、 存储程序的悲观锁的信息(假如使用了悲观锁) +-- ---------------------------- +drop table if exists QRTZ_LOCKS; +create table QRTZ_LOCKS ( + sched_name varchar(120) not null, + lock_name varchar(40) not null, + primary key (sched_name,lock_name) +) engine=innodb; + +drop table if exists QRTZ_SIMPROP_TRIGGERS; +create table QRTZ_SIMPROP_TRIGGERS ( + sched_name varchar(120) not null, + trigger_name varchar(200) not null, + trigger_group varchar(200) not null, + str_prop_1 varchar(512) null, + str_prop_2 varchar(512) null, + str_prop_3 varchar(512) null, + int_prop_1 int null, + int_prop_2 int null, + long_prop_1 bigint null, + long_prop_2 bigint null, + dec_prop_1 numeric(13,4) null, + dec_prop_2 numeric(13,4) null, + bool_prop_1 varchar(1) null, + bool_prop_2 varchar(1) null, + primary key (sched_name,trigger_name,trigger_group), + foreign key (sched_name,trigger_name,trigger_group) references QRTZ_TRIGGERS(sched_name,trigger_name,trigger_group) +) engine=innodb; + +commit; \ No newline at end of file diff --git a/sql/refresh.sql b/sql/refresh.sql new file mode 100644 index 0000000..4b566c3 --- /dev/null +++ b/sql/refresh.sql @@ -0,0 +1,33 @@ +## 1. 清空用户表 +## 排除 admin,ry,591管理员 +delete from sys_user where user_id not in(1,2,109); + +## 2. 清空业务表 +truncate table bus_customer; +truncate table bus_customer_address; +truncate table bus_customer_coupon; +truncate table bus_customer_star; + +truncate table bus_mechanic; +truncate table bus_mechanic_info; +truncate table bus_join_mechanic; +truncate table bus_health_record; + +truncate table bus_shop; +truncate table bus_shop_amount_logs; +truncate table bus_shop_in_logs; +truncate table bus_shop_withdraw; +truncate table bus_join_shop; +truncate table bus_item_assign; +truncate table bus_service_item; + +truncate table bus_order; +truncate table bus_order_delete_bak; +truncate table bus_order_logs; +truncate table bus_vip_order; +truncate table bus_evaluation; + +truncate table bus_qualification_apply; + + + diff --git a/sql/ry_20210210.sql b/sql/ry_20210210.sql new file mode 100644 index 0000000..5dc16c1 --- /dev/null +++ b/sql/ry_20210210.sql @@ -0,0 +1,688 @@ +-- ---------------------------- +-- 1、部门表 +-- ---------------------------- +drop table if exists sys_dept; +create table sys_dept ( + dept_id bigint(20) not null auto_increment comment '部门id', + parent_id bigint(20) default 0 comment '父部门id', + ancestors varchar(50) default '' comment '祖级列表', + dept_name varchar(30) default '' comment '部门名称', + order_num int(4) default 0 comment '显示顺序', + leader varchar(20) default null comment '负责人', + phone varchar(11) default null comment '联系电话', + email varchar(50) default null comment '邮箱', + status char(1) default '0' comment '部门状态(0正常 1停用)', + del_flag char(1) default '0' comment '删除标志(0代表存在 2代表删除)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + primary key (dept_id) +) engine=innodb auto_increment=200 comment = '部门表'; + +-- ---------------------------- +-- 初始化-部门表数据 +-- ---------------------------- +insert into sys_dept values(100, 0, '0', '若依科技', 0, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(101, 100, '0,100', '深圳总公司', 1, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(102, 100, '0,100', '长沙分公司', 2, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(103, 101, '0,100,101', '研发部门', 1, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(104, 101, '0,100,101', '市场部门', 2, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(105, 101, '0,100,101', '测试部门', 3, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(106, 101, '0,100,101', '财务部门', 4, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(107, 101, '0,100,101', '运维部门', 5, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(108, 102, '0,100,102', '市场部门', 1, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); +insert into sys_dept values(109, 102, '0,100,102', '财务部门', 2, '若依', '15888888888', 'ry@qq.com', '0', '0', 'admin', sysdate(), '', null); + + +-- ---------------------------- +-- 2、用户信息表 +-- ---------------------------- +drop table if exists sys_user; +create table sys_user ( + user_id bigint(20) not null auto_increment comment '用户ID', + dept_id bigint(20) default null comment '部门ID', + user_name varchar(30) not null comment '用户账号', + nick_name varchar(30) not null comment '用户昵称', + user_type varchar(2) default '00' comment '用户类型(00系统用户)', + email varchar(50) default '' comment '用户邮箱', + phonenumber varchar(11) default '' comment '手机号码', + sex char(1) default '0' comment '用户性别(0男 1女 2未知)', + avatar varchar(100) default '' comment '头像地址', + password varchar(100) default '' comment '密码', + status char(1) default '0' comment '帐号状态(0正常 1停用)', + del_flag char(1) default '0' comment '删除标志(0代表存在 2代表删除)', + login_ip varchar(128) default '' comment '最后登录IP', + login_date datetime comment '最后登录时间', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (user_id) +) engine=innodb auto_increment=100 comment = '用户信息表'; + +-- ---------------------------- +-- 初始化-用户信息表数据 +-- ---------------------------- +insert into sys_user values(1, 103, 'admin', '若依', '00', 'ry@163.com', '15888888888', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', sysdate(), 'admin', sysdate(), '', null, '管理员'); +insert into sys_user values(2, 105, 'ry', '若依', '00', 'ry@qq.com', '15666666666', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', sysdate(), 'admin', sysdate(), '', null, '测试员'); + + +-- ---------------------------- +-- 3、岗位信息表 +-- ---------------------------- +drop table if exists sys_post; +create table sys_post +( + post_id bigint(20) not null auto_increment comment '岗位ID', + post_code varchar(64) not null comment '岗位编码', + post_name varchar(50) not null comment '岗位名称', + post_sort int(4) not null comment '显示顺序', + status char(1) not null comment '状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (post_id) +) engine=innodb comment = '岗位信息表'; + +-- ---------------------------- +-- 初始化-岗位信息表数据 +-- ---------------------------- +insert into sys_post values(1, 'ceo', '董事长', 1, '0', 'admin', sysdate(), '', null, ''); +insert into sys_post values(2, 'se', '项目经理', 2, '0', 'admin', sysdate(), '', null, ''); +insert into sys_post values(3, 'hr', '人力资源', 3, '0', 'admin', sysdate(), '', null, ''); +insert into sys_post values(4, 'user', '普通员工', 4, '0', 'admin', sysdate(), '', null, ''); + + +-- ---------------------------- +-- 4、角色信息表 +-- ---------------------------- +drop table if exists sys_role; +create table sys_role ( + role_id bigint(20) not null auto_increment comment '角色ID', + role_name varchar(30) not null comment '角色名称', + role_key varchar(100) not null comment '角色权限字符串', + role_sort int(4) not null comment '显示顺序', + data_scope char(1) default '1' comment '数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)', + menu_check_strictly tinyint(1) default 1 comment '菜单树选择项是否关联显示', + dept_check_strictly tinyint(1) default 1 comment '部门树选择项是否关联显示', + status char(1) not null comment '角色状态(0正常 1停用)', + del_flag char(1) default '0' comment '删除标志(0代表存在 2代表删除)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (role_id) +) engine=innodb auto_increment=100 comment = '角色信息表'; + +-- ---------------------------- +-- 初始化-角色信息表数据 +-- ---------------------------- +insert into sys_role values('1', '超级管理员', 'admin', 1, 1, 1, 1, '0', '0', 'admin', sysdate(), '', null, '超级管理员'); +insert into sys_role values('2', '普通角色', 'common', 2, 2, 1, 1, '0', '0', 'admin', sysdate(), '', null, '普通角色'); + + +-- ---------------------------- +-- 5、菜单权限表 +-- ---------------------------- +drop table if exists sys_menu; +create table sys_menu ( + menu_id bigint(20) not null auto_increment comment '菜单ID', + menu_name varchar(50) not null comment '菜单名称', + parent_id bigint(20) default 0 comment '父菜单ID', + order_num int(4) default 0 comment '显示顺序', + path varchar(200) default '' comment '路由地址', + component varchar(255) default null comment '组件路径', + is_frame int(1) default 1 comment '是否为外链(0是 1否)', + is_cache int(1) default 0 comment '是否缓存(0缓存 1不缓存)', + menu_type char(1) default '' comment '菜单类型(M目录 C菜单 F按钮)', + visible char(1) default 0 comment '菜单状态(0显示 1隐藏)', + status char(1) default 0 comment '菜单状态(0正常 1停用)', + perms varchar(100) default null comment '权限标识', + icon varchar(100) default '#' comment '菜单图标', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default '' comment '备注', + primary key (menu_id) +) engine=innodb auto_increment=2000 comment = '菜单权限表'; + +-- ---------------------------- +-- 初始化-菜单信息表数据 +-- ---------------------------- +-- 一级菜单 +insert into sys_menu values('1', '系统管理', '0', '1', 'system', null, 1, 0, 'M', '0', '0', '', 'system', 'admin', sysdate(), '', null, '系统管理目录'); +insert into sys_menu values('2', '系统监控', '0', '2', 'monitor', null, 1, 0, 'M', '0', '0', '', 'monitor', 'admin', sysdate(), '', null, '系统监控目录'); +insert into sys_menu values('3', '系统工具', '0', '3', 'tool', null, 1, 0, 'M', '0', '0', '', 'tool', 'admin', sysdate(), '', null, '系统工具目录'); +insert into sys_menu values('4', '若依官网', '0', '4', 'http://.vip', null , 0, 0, 'M', '0', '0', '', 'guide', 'admin', sysdate(), '', null, '若依官网地址'); +-- 二级菜单 +insert into sys_menu values('100', '用户管理', '1', '1', 'user', 'system/user/index', 1, 0, 'C', '0', '0', 'system:user:list', 'user', 'admin', sysdate(), '', null, '用户管理菜单'); +insert into sys_menu values('101', '角色管理', '1', '2', 'role', 'system/role/index', 1, 0, 'C', '0', '0', 'system:role:list', 'peoples', 'admin', sysdate(), '', null, '角色管理菜单'); +insert into sys_menu values('102', '菜单管理', '1', '3', 'menu', 'system/menu/index', 1, 0, 'C', '0', '0', 'system:menu:list', 'tree-table', 'admin', sysdate(), '', null, '菜单管理菜单'); +insert into sys_menu values('103', '部门管理', '1', '4', 'dept', 'system/dept/index', 1, 0, 'C', '0', '0', 'system:dept:list', 'tree', 'admin', sysdate(), '', null, '部门管理菜单'); +insert into sys_menu values('104', '岗位管理', '1', '5', 'post', 'system/post/index', 1, 0, 'C', '0', '0', 'system:post:list', 'post', 'admin', sysdate(), '', null, '岗位管理菜单'); +insert into sys_menu values('105', '字典管理', '1', '6', 'dict', 'system/dict/index', 1, 0, 'C', '0', '0', 'system:dict:list', 'dict', 'admin', sysdate(), '', null, '字典管理菜单'); +insert into sys_menu values('106', '参数设置', '1', '7', 'config', 'system/config/index', 1, 0, 'C', '0', '0', 'system:config:list', 'edit', 'admin', sysdate(), '', null, '参数设置菜单'); +insert into sys_menu values('107', '通知公告', '1', '8', 'notice', 'system/notice/index', 1, 0, 'C', '0', '0', 'system:notice:list', 'message', 'admin', sysdate(), '', null, '通知公告菜单'); +insert into sys_menu values('108', '日志管理', '1', '9', 'log', '', 1, 0, 'M', '0', '0', '', 'log', 'admin', sysdate(), '', null, '日志管理菜单'); +insert into sys_menu values('109', '在线用户', '2', '1', 'online', 'monitor/online/index', 1, 0, 'C', '0', '0', 'monitor:online:list', 'online', 'admin', sysdate(), '', null, '在线用户菜单'); +insert into sys_menu values('110', '定时任务', '2', '2', 'job', 'monitor/job/index', 1, 0, 'C', '0', '0', 'monitor:job:list', 'job', 'admin', sysdate(), '', null, '定时任务菜单'); +insert into sys_menu values('111', '数据监控', '2', '3', 'druid', 'monitor/druid/index', 1, 0, 'C', '0', '0', 'monitor:druid:list', 'druid', 'admin', sysdate(), '', null, '数据监控菜单'); +# insert into sys_menu values('112', '服务监控', '2', '4', 'server', 'monitor/server/index', 1, 0, 'C', '0', '0', 'monitor:server:list', 'server', 'admin', sysdate(), '', null, '服务监控菜单'); +insert into sys_menu values('113', '缓存监控', '2', '5', 'cache', 'monitor/cache/index', 1, 0, 'C', '0', '0', 'monitor:cache:list', 'redis', 'admin', sysdate(), '', null, '缓存监控菜单'); +insert into sys_menu values('114', '表单构建', '3', '1', 'build', 'tool/build/index', 1, 0, 'C', '0', '0', 'tool:build:list', 'build', 'admin', sysdate(), '', null, '表单构建菜单'); +insert into sys_menu values('115', '代码生成', '3', '2', 'gen', 'tool/gen/index', 1, 0, 'C', '0', '0', 'tool:gen:list', 'code', 'admin', sysdate(), '', null, '代码生成菜单'); +insert into sys_menu values('116', '系统接口', '3', '3', 'swagger', 'tool/swagger/index', 1, 0, 'C', '0', '0', 'tool:swagger:list', 'swagger', 'admin', sysdate(), '', null, '系统接口菜单'); +-- springboot-admin监控 +insert into sys_menu values('117', 'Admin监控', '2', '5', 'Admin', 'monitor/admin/index', 1, 0, 'C', '0', '0', 'monitor:admin:list', 'dashboard', 'admin', sysdate(), '', null, 'Admin监控菜单'); + +-- 三级菜单 +insert into sys_menu values('500', '操作日志', '108', '1', 'operlog', 'monitor/operlog/index', 1, 0, 'C', '0', '0', 'monitor:operlog:list', 'form', 'admin', sysdate(), '', null, '操作日志菜单'); +insert into sys_menu values('501', '登录日志', '108', '2', 'logininfor', 'monitor/logininfor/index', 1, 0, 'C', '0', '0', 'monitor:logininfor:list', 'logininfor', 'admin', sysdate(), '', null, '登录日志菜单'); +-- 用户管理按钮 +insert into sys_menu values('1001', '用户查询', '100', '1', '', '', 1, 0, 'F', '0', '0', 'system:user:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1002', '用户新增', '100', '2', '', '', 1, 0, 'F', '0', '0', 'system:user:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1003', '用户修改', '100', '3', '', '', 1, 0, 'F', '0', '0', 'system:user:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1004', '用户删除', '100', '4', '', '', 1, 0, 'F', '0', '0', 'system:user:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1005', '用户导出', '100', '5', '', '', 1, 0, 'F', '0', '0', 'system:user:export', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1006', '用户导入', '100', '6', '', '', 1, 0, 'F', '0', '0', 'system:user:import', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1007', '重置密码', '100', '7', '', '', 1, 0, 'F', '0', '0', 'system:user:resetPwd', '#', 'admin', sysdate(), '', null, ''); +-- 角色管理按钮 +insert into sys_menu values('1008', '角色查询', '101', '1', '', '', 1, 0, 'F', '0', '0', 'system:role:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1009', '角色新增', '101', '2', '', '', 1, 0, 'F', '0', '0', 'system:role:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1010', '角色修改', '101', '3', '', '', 1, 0, 'F', '0', '0', 'system:role:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1011', '角色删除', '101', '4', '', '', 1, 0, 'F', '0', '0', 'system:role:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1012', '角色导出', '101', '5', '', '', 1, 0, 'F', '0', '0', 'system:role:export', '#', 'admin', sysdate(), '', null, ''); +-- 菜单管理按钮 +insert into sys_menu values('1013', '菜单查询', '102', '1', '', '', 1, 0, 'F', '0', '0', 'system:menu:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1014', '菜单新增', '102', '2', '', '', 1, 0, 'F', '0', '0', 'system:menu:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1015', '菜单修改', '102', '3', '', '', 1, 0, 'F', '0', '0', 'system:menu:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1016', '菜单删除', '102', '4', '', '', 1, 0, 'F', '0', '0', 'system:menu:remove', '#', 'admin', sysdate(), '', null, ''); +-- 部门管理按钮 +insert into sys_menu values('1017', '部门查询', '103', '1', '', '', 1, 0, 'F', '0', '0', 'system:dept:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1018', '部门新增', '103', '2', '', '', 1, 0, 'F', '0', '0', 'system:dept:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1019', '部门修改', '103', '3', '', '', 1, 0, 'F', '0', '0', 'system:dept:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1020', '部门删除', '103', '4', '', '', 1, 0, 'F', '0', '0', 'system:dept:remove', '#', 'admin', sysdate(), '', null, ''); +-- 岗位管理按钮 +insert into sys_menu values('1021', '岗位查询', '104', '1', '', '', 1, 0, 'F', '0', '0', 'system:post:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1022', '岗位新增', '104', '2', '', '', 1, 0, 'F', '0', '0', 'system:post:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1023', '岗位修改', '104', '3', '', '', 1, 0, 'F', '0', '0', 'system:post:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1024', '岗位删除', '104', '4', '', '', 1, 0, 'F', '0', '0', 'system:post:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1025', '岗位导出', '104', '5', '', '', 1, 0, 'F', '0', '0', 'system:post:export', '#', 'admin', sysdate(), '', null, ''); +-- 字典管理按钮 +insert into sys_menu values('1026', '字典查询', '105', '1', '#', '', 1, 0, 'F', '0', '0', 'system:dict:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1027', '字典新增', '105', '2', '#', '', 1, 0, 'F', '0', '0', 'system:dict:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1028', '字典修改', '105', '3', '#', '', 1, 0, 'F', '0', '0', 'system:dict:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1029', '字典删除', '105', '4', '#', '', 1, 0, 'F', '0', '0', 'system:dict:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1030', '字典导出', '105', '5', '#', '', 1, 0, 'F', '0', '0', 'system:dict:export', '#', 'admin', sysdate(), '', null, ''); +-- 参数设置按钮 +insert into sys_menu values('1031', '参数查询', '106', '1', '#', '', 1, 0, 'F', '0', '0', 'system:config:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1032', '参数新增', '106', '2', '#', '', 1, 0, 'F', '0', '0', 'system:config:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1033', '参数修改', '106', '3', '#', '', 1, 0, 'F', '0', '0', 'system:config:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1034', '参数删除', '106', '4', '#', '', 1, 0, 'F', '0', '0', 'system:config:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1035', '参数导出', '106', '5', '#', '', 1, 0, 'F', '0', '0', 'system:config:export', '#', 'admin', sysdate(), '', null, ''); +-- 通知公告按钮 +insert into sys_menu values('1036', '公告查询', '107', '1', '#', '', 1, 0, 'F', '0', '0', 'system:notice:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1037', '公告新增', '107', '2', '#', '', 1, 0, 'F', '0', '0', 'system:notice:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1038', '公告修改', '107', '3', '#', '', 1, 0, 'F', '0', '0', 'system:notice:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1039', '公告删除', '107', '4', '#', '', 1, 0, 'F', '0', '0', 'system:notice:remove', '#', 'admin', sysdate(), '', null, ''); +-- 操作日志按钮 +insert into sys_menu values('1040', '操作查询', '500', '1', '#', '', 1, 0, 'F', '0', '0', 'monitor:operlog:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1041', '操作删除', '500', '2', '#', '', 1, 0, 'F', '0', '0', 'monitor:operlog:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1042', '日志导出', '500', '4', '#', '', 1, 0, 'F', '0', '0', 'monitor:operlog:export', '#', 'admin', sysdate(), '', null, ''); +-- 登录日志按钮 +insert into sys_menu values('1043', '登录查询', '501', '1', '#', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1044', '登录删除', '501', '2', '#', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1045', '日志导出', '501', '3', '#', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:export', '#', 'admin', sysdate(), '', null, ''); +-- 在线用户按钮 +insert into sys_menu values('1046', '在线查询', '109', '1', '#', '', 1, 0, 'F', '0', '0', 'monitor:online:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1047', '批量强退', '109', '2', '#', '', 1, 0, 'F', '0', '0', 'monitor:online:batchLogout', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1048', '单条强退', '109', '3', '#', '', 1, 0, 'F', '0', '0', 'monitor:online:forceLogout', '#', 'admin', sysdate(), '', null, ''); +-- 定时任务按钮 +insert into sys_menu values('1049', '任务查询', '110', '1', '#', '', 1, 0, 'F', '0', '0', 'monitor:job:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1050', '任务新增', '110', '2', '#', '', 1, 0, 'F', '0', '0', 'monitor:job:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1051', '任务修改', '110', '3', '#', '', 1, 0, 'F', '0', '0', 'monitor:job:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1052', '任务删除', '110', '4', '#', '', 1, 0, 'F', '0', '0', 'monitor:job:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1053', '状态修改', '110', '5', '#', '', 1, 0, 'F', '0', '0', 'monitor:job:changeStatus', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1054', '任务导出', '110', '7', '#', '', 1, 0, 'F', '0', '0', 'monitor:job:export', '#', 'admin', sysdate(), '', null, ''); +-- 代码生成按钮 +insert into sys_menu values('1055', '生成查询', '115', '1', '#', '', 1, 0, 'F', '0', '0', 'tool:gen:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1056', '生成修改', '115', '2', '#', '', 1, 0, 'F', '0', '0', 'tool:gen:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1057', '生成删除', '115', '3', '#', '', 1, 0, 'F', '0', '0', 'tool:gen:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1058', '导入代码', '115', '2', '#', '', 1, 0, 'F', '0', '0', 'tool:gen:import', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1059', '预览代码', '115', '4', '#', '', 1, 0, 'F', '0', '0', 'tool:gen:preview', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1060', '生成代码', '115', '5', '#', '', 1, 0, 'F', '0', '0', 'tool:gen:code', '#', 'admin', sysdate(), '', null, ''); + + +-- ---------------------------- +-- 6、用户和角色关联表 用户N-1角色 +-- ---------------------------- +drop table if exists sys_user_role; +create table sys_user_role ( + user_id bigint(20) not null comment '用户ID', + role_id bigint(20) not null comment '角色ID', + primary key(user_id, role_id) +) engine=innodb comment = '用户和角色关联表'; + +-- ---------------------------- +-- 初始化-用户和角色关联表数据 +-- ---------------------------- +insert into sys_user_role values ('1', '1'); +insert into sys_user_role values ('2', '2'); + + +-- ---------------------------- +-- 7、角色和菜单关联表 角色1-N菜单 +-- ---------------------------- +drop table if exists sys_role_menu; +create table sys_role_menu ( + role_id bigint(20) not null comment '角色ID', + menu_id bigint(20) not null comment '菜单ID', + primary key(role_id, menu_id) +) engine=innodb comment = '角色和菜单关联表'; + +-- ---------------------------- +-- 初始化-角色和菜单关联表数据 +-- ---------------------------- +insert into sys_role_menu values ('2', '1'); +insert into sys_role_menu values ('2', '2'); +insert into sys_role_menu values ('2', '3'); +insert into sys_role_menu values ('2', '4'); +insert into sys_role_menu values ('2', '100'); +insert into sys_role_menu values ('2', '101'); +insert into sys_role_menu values ('2', '102'); +insert into sys_role_menu values ('2', '103'); +insert into sys_role_menu values ('2', '104'); +insert into sys_role_menu values ('2', '105'); +insert into sys_role_menu values ('2', '106'); +insert into sys_role_menu values ('2', '107'); +insert into sys_role_menu values ('2', '108'); +insert into sys_role_menu values ('2', '109'); +insert into sys_role_menu values ('2', '110'); +insert into sys_role_menu values ('2', '111'); +insert into sys_role_menu values ('2', '112'); +insert into sys_role_menu values ('2', '113'); +insert into sys_role_menu values ('2', '114'); +insert into sys_role_menu values ('2', '115'); +insert into sys_role_menu values ('2', '116'); +insert into sys_role_menu values ('2', '500'); +insert into sys_role_menu values ('2', '501'); +insert into sys_role_menu values ('2', '1000'); +insert into sys_role_menu values ('2', '1001'); +insert into sys_role_menu values ('2', '1002'); +insert into sys_role_menu values ('2', '1003'); +insert into sys_role_menu values ('2', '1004'); +insert into sys_role_menu values ('2', '1005'); +insert into sys_role_menu values ('2', '1006'); +insert into sys_role_menu values ('2', '1007'); +insert into sys_role_menu values ('2', '1008'); +insert into sys_role_menu values ('2', '1009'); +insert into sys_role_menu values ('2', '1010'); +insert into sys_role_menu values ('2', '1011'); +insert into sys_role_menu values ('2', '1012'); +insert into sys_role_menu values ('2', '1013'); +insert into sys_role_menu values ('2', '1014'); +insert into sys_role_menu values ('2', '1015'); +insert into sys_role_menu values ('2', '1016'); +insert into sys_role_menu values ('2', '1017'); +insert into sys_role_menu values ('2', '1018'); +insert into sys_role_menu values ('2', '1019'); +insert into sys_role_menu values ('2', '1020'); +insert into sys_role_menu values ('2', '1021'); +insert into sys_role_menu values ('2', '1022'); +insert into sys_role_menu values ('2', '1023'); +insert into sys_role_menu values ('2', '1024'); +insert into sys_role_menu values ('2', '1025'); +insert into sys_role_menu values ('2', '1026'); +insert into sys_role_menu values ('2', '1027'); +insert into sys_role_menu values ('2', '1028'); +insert into sys_role_menu values ('2', '1029'); +insert into sys_role_menu values ('2', '1030'); +insert into sys_role_menu values ('2', '1031'); +insert into sys_role_menu values ('2', '1032'); +insert into sys_role_menu values ('2', '1033'); +insert into sys_role_menu values ('2', '1034'); +insert into sys_role_menu values ('2', '1035'); +insert into sys_role_menu values ('2', '1036'); +insert into sys_role_menu values ('2', '1037'); +insert into sys_role_menu values ('2', '1038'); +insert into sys_role_menu values ('2', '1039'); +insert into sys_role_menu values ('2', '1040'); +insert into sys_role_menu values ('2', '1041'); +insert into sys_role_menu values ('2', '1042'); +insert into sys_role_menu values ('2', '1043'); +insert into sys_role_menu values ('2', '1044'); +insert into sys_role_menu values ('2', '1045'); +insert into sys_role_menu values ('2', '1046'); +insert into sys_role_menu values ('2', '1047'); +insert into sys_role_menu values ('2', '1048'); +insert into sys_role_menu values ('2', '1049'); +insert into sys_role_menu values ('2', '1050'); +insert into sys_role_menu values ('2', '1051'); +insert into sys_role_menu values ('2', '1052'); +insert into sys_role_menu values ('2', '1053'); +insert into sys_role_menu values ('2', '1054'); +insert into sys_role_menu values ('2', '1055'); +insert into sys_role_menu values ('2', '1056'); +insert into sys_role_menu values ('2', '1057'); +insert into sys_role_menu values ('2', '1058'); +insert into sys_role_menu values ('2', '1059'); +insert into sys_role_menu values ('2', '1060'); + +-- ---------------------------- +-- 8、角色和部门关联表 角色1-N部门 +-- ---------------------------- +drop table if exists sys_role_dept; +create table sys_role_dept ( + role_id bigint(20) not null comment '角色ID', + dept_id bigint(20) not null comment '部门ID', + primary key(role_id, dept_id) +) engine=innodb comment = '角色和部门关联表'; + +-- ---------------------------- +-- 初始化-角色和部门关联表数据 +-- ---------------------------- +insert into sys_role_dept values ('2', '100'); +insert into sys_role_dept values ('2', '101'); +insert into sys_role_dept values ('2', '105'); + + +-- ---------------------------- +-- 9、用户与岗位关联表 用户1-N岗位 +-- ---------------------------- +drop table if exists sys_user_post; +create table sys_user_post +( + user_id bigint(20) not null comment '用户ID', + post_id bigint(20) not null comment '岗位ID', + primary key (user_id, post_id) +) engine=innodb comment = '用户与岗位关联表'; + +-- ---------------------------- +-- 初始化-用户与岗位关联表数据 +-- ---------------------------- +insert into sys_user_post values ('1', '1'); +insert into sys_user_post values ('2', '2'); + + +-- ---------------------------- +-- 10、操作日志记录 +-- ---------------------------- +drop table if exists sys_oper_log; +create table sys_oper_log ( + oper_id bigint(20) not null auto_increment comment '日志主键', + title varchar(50) default '' comment '模块标题', + business_type int(2) default 0 comment '业务类型(0其它 1新增 2修改 3删除)', + method varchar(100) default '' comment '方法名称', + request_method varchar(10) default '' comment '请求方式', + operator_type int(1) default 0 comment '操作类别(0其它 1后台用户 2手机端用户)', + oper_name varchar(50) default '' comment '操作人员', + dept_name varchar(50) default '' comment '部门名称', + oper_url varchar(255) default '' comment '请求URL', + oper_ip varchar(128) default '' comment '主机地址', + oper_location varchar(255) default '' comment '操作地点', + oper_param varchar(2000) default '' comment '请求参数', + json_result varchar(2000) default '' comment '返回参数', + status int(1) default 0 comment '操作状态(0正常 1异常)', + error_msg varchar(2000) default '' comment '错误消息', + oper_time datetime comment '操作时间', + primary key (oper_id) +) engine=innodb auto_increment=100 comment = '操作日志记录'; + + +-- ---------------------------- +-- 11、字典类型表 +-- ---------------------------- +drop table if exists sys_dict_type; +create table sys_dict_type +( + dict_id bigint(20) not null auto_increment comment '字典主键', + dict_name varchar(100) default '' comment '字典名称', + dict_type varchar(100) default '' comment '字典类型', + status char(1) default '0' comment '状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (dict_id), + unique (dict_type) +) engine=innodb auto_increment=100 comment = '字典类型表'; + +insert into sys_dict_type values(1, '用户性别', 'sys_user_sex', '0', 'admin', sysdate(), '', null, '用户性别列表'); +insert into sys_dict_type values(2, '菜单状态', 'sys_show_hide', '0', 'admin', sysdate(), '', null, '菜单状态列表'); +insert into sys_dict_type values(3, '系统开关', 'sys_normal_disable', '0', 'admin', sysdate(), '', null, '系统开关列表'); +insert into sys_dict_type values(4, '任务状态', 'sys_job_status', '0', 'admin', sysdate(), '', null, '任务状态列表'); +insert into sys_dict_type values(5, '任务分组', 'sys_job_group', '0', 'admin', sysdate(), '', null, '任务分组列表'); +insert into sys_dict_type values(6, '系统是否', 'sys_yes_no', '0', 'admin', sysdate(), '', null, '系统是否列表'); +insert into sys_dict_type values(7, '通知类型', 'sys_notice_type', '0', 'admin', sysdate(), '', null, '通知类型列表'); +insert into sys_dict_type values(8, '通知状态', 'sys_notice_status', '0', 'admin', sysdate(), '', null, '通知状态列表'); +insert into sys_dict_type values(9, '操作类型', 'sys_oper_type', '0', 'admin', sysdate(), '', null, '操作类型列表'); +insert into sys_dict_type values(10, '系统状态', 'sys_common_status', '0', 'admin', sysdate(), '', null, '登录状态列表'); + + +-- ---------------------------- +-- 12、字典数据表 +-- ---------------------------- +drop table if exists sys_dict_data; +create table sys_dict_data +( + dict_code bigint(20) not null auto_increment comment '字典编码', + dict_sort int(4) default 0 comment '字典排序', + dict_label varchar(100) default '' comment '字典标签', + dict_value varchar(100) default '' comment '字典键值', + dict_type varchar(100) default '' comment '字典类型', + css_class varchar(100) default null comment '样式属性(其他样式扩展)', + list_class varchar(100) default null comment '表格回显样式', + is_default char(1) default 'N' comment '是否默认(Y是 N否)', + status char(1) default '0' comment '状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (dict_code) +) engine=innodb auto_increment=100 comment = '字典数据表'; + +insert into sys_dict_data values(1, 1, '男', '0', 'sys_user_sex', '', '', 'Y', '0', 'admin', sysdate(), '', null, '性别男'); +insert into sys_dict_data values(2, 2, '女', '1', 'sys_user_sex', '', '', 'N', '0', 'admin', sysdate(), '', null, '性别女'); +insert into sys_dict_data values(3, 3, '未知', '2', 'sys_user_sex', '', '', 'N', '0', 'admin', sysdate(), '', null, '性别未知'); +insert into sys_dict_data values(4, 1, '显示', '0', 'sys_show_hide', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '显示菜单'); +insert into sys_dict_data values(5, 2, '隐藏', '1', 'sys_show_hide', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '隐藏菜单'); +insert into sys_dict_data values(6, 1, '正常', '0', 'sys_normal_disable', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '正常状态'); +insert into sys_dict_data values(7, 2, '停用', '1', 'sys_normal_disable', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '停用状态'); +insert into sys_dict_data values(8, 1, '正常', '0', 'sys_job_status', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '正常状态'); +insert into sys_dict_data values(9, 2, '暂停', '1', 'sys_job_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '停用状态'); +insert into sys_dict_data values(10, 1, '默认', 'DEFAULT', 'sys_job_group', '', '', 'Y', '0', 'admin', sysdate(), '', null, '默认分组'); +insert into sys_dict_data values(11, 2, '系统', 'SYSTEM', 'sys_job_group', '', '', 'N', '0', 'admin', sysdate(), '', null, '系统分组'); +insert into sys_dict_data values(12, 1, '是', 'Y', 'sys_yes_no', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '系统默认是'); +insert into sys_dict_data values(13, 2, '否', 'N', 'sys_yes_no', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '系统默认否'); +insert into sys_dict_data values(14, 1, '通知', '1', 'sys_notice_type', '', 'warning', 'Y', '0', 'admin', sysdate(), '', null, '通知'); +insert into sys_dict_data values(15, 2, '公告', '2', 'sys_notice_type', '', 'success', 'N', '0', 'admin', sysdate(), '', null, '公告'); +insert into sys_dict_data values(16, 1, '正常', '0', 'sys_notice_status', '', 'primary', 'Y', '0', 'admin', sysdate(), '', null, '正常状态'); +insert into sys_dict_data values(17, 2, '关闭', '1', 'sys_notice_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '关闭状态'); +insert into sys_dict_data values(18, 1, '新增', '1', 'sys_oper_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, '新增操作'); +insert into sys_dict_data values(19, 2, '修改', '2', 'sys_oper_type', '', 'info', 'N', '0', 'admin', sysdate(), '', null, '修改操作'); +insert into sys_dict_data values(20, 3, '删除', '3', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '删除操作'); +insert into sys_dict_data values(21, 4, '授权', '4', 'sys_oper_type', '', 'primary', 'N', '0', 'admin', sysdate(), '', null, '授权操作'); +insert into sys_dict_data values(22, 5, '导出', '5', 'sys_oper_type', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '导出操作'); +insert into sys_dict_data values(23, 6, '导入', '6', 'sys_oper_type', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '导入操作'); +insert into sys_dict_data values(24, 7, '强退', '7', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '强退操作'); +insert into sys_dict_data values(25, 8, '生成代码', '8', 'sys_oper_type', '', 'warning', 'N', '0', 'admin', sysdate(), '', null, '生成操作'); +insert into sys_dict_data values(26, 9, '清空数据', '9', 'sys_oper_type', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '清空操作'); +insert into sys_dict_data values(27, 1, '成功', '0', 'sys_common_status', '', 'primary', 'N', '0', 'admin', sysdate(), '', null, '正常状态'); +insert into sys_dict_data values(28, 2, '失败', '1', 'sys_common_status', '', 'danger', 'N', '0', 'admin', sysdate(), '', null, '停用状态'); + + +-- ---------------------------- +-- 13、参数配置表 +-- ---------------------------- +drop table if exists sys_config; +create table sys_config ( + config_id int(5) not null auto_increment comment '参数主键', + config_name varchar(100) default '' comment '参数名称', + config_key varchar(100) default '' comment '参数键名', + config_value varchar(500) default '' comment '参数键值', + config_type char(1) default 'N' comment '系统内置(Y是 N否)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (config_id) +) engine=innodb auto_increment=100 comment = '参数配置表'; + +insert into sys_config values(1, '主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', 'admin', sysdate(), '', null, '蓝色 skin-blue、绿色 skin-green、紫色 skin-purple、红色 skin-red、黄色 skin-yellow' ); +insert into sys_config values(2, '用户管理-账号初始密码', 'sys.user.initPassword', '123456', 'Y', 'admin', sysdate(), '', null, '初始化密码 123456' ); +insert into sys_config values(3, '主框架页-侧边栏主题', 'sys.index.sideTheme', 'theme-dark', 'Y', 'admin', sysdate(), '', null, '深色主题theme-dark,浅色主题theme-light' ); + + +-- ---------------------------- +-- 14、系统访问记录 +-- ---------------------------- +drop table if exists sys_logininfor; +create table sys_logininfor ( + info_id bigint(20) not null auto_increment comment '访问ID', + user_name varchar(50) default '' comment '用户账号', + ipaddr varchar(128) default '' comment '登录IP地址', + login_location varchar(255) default '' comment '登录地点', + browser varchar(50) default '' comment '浏览器类型', + os varchar(50) default '' comment '操作系统', + status char(1) default '0' comment '登录状态(0成功 1失败)', + msg varchar(255) default '' comment '提示消息', + login_time datetime comment '访问时间', + primary key (info_id) +) engine=innodb auto_increment=100 comment = '系统访问记录'; + + +-- ---------------------------- +-- 15、定时任务调度表 +-- ---------------------------- +drop table if exists sys_job; +create table sys_job ( + job_id bigint(20) not null auto_increment comment '任务ID', + job_name varchar(64) default '' comment '任务名称', + job_group varchar(64) default 'DEFAULT' comment '任务组名', + invoke_target varchar(500) not null comment '调用目标字符串', + cron_expression varchar(255) default '' comment 'cron执行表达式', + misfire_policy varchar(20) default '3' comment '计划执行错误策略(1立即执行 2执行一次 3放弃执行)', + concurrent char(1) default '1' comment '是否并发执行(0允许 1禁止)', + status char(1) default '0' comment '状态(0正常 1暂停)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default '' comment '备注信息', + primary key (job_id, job_name, job_group) +) engine=innodb auto_increment=100 comment = '定时任务调度表'; + +insert into sys_job values(1, '系统默认(无参)', 'DEFAULT', 'ryTask.ryNoParams', '0/10 * * * * ?', '3', '1', '1', 'admin', sysdate(), '', null, ''); +insert into sys_job values(2, '系统默认(有参)', 'DEFAULT', 'ryTask.ryParams(\'ry\')', '0/15 * * * * ?', '3', '1', '1', 'admin', sysdate(), '', null, ''); +insert into sys_job values(3, '系统默认(多参)', 'DEFAULT', 'ryTask.ryMultipleParams(\'ry\', true, 2000L, 316.50D, 100)', '0/20 * * * * ?', '3', '1', '1', 'admin', sysdate(), '', null, ''); + + +-- ---------------------------- +-- 16、定时任务调度日志表 +-- ---------------------------- +drop table if exists sys_job_log; +create table sys_job_log ( + job_log_id bigint(20) not null auto_increment comment '任务日志ID', + job_name varchar(64) not null comment '任务名称', + job_group varchar(64) not null comment '任务组名', + invoke_target varchar(500) not null comment '调用目标字符串', + job_message varchar(500) comment '日志信息', + status char(1) default '0' comment '执行状态(0正常 1失败)', + exception_info varchar(2000) default '' comment '异常信息', + create_time datetime comment '创建时间', + primary key (job_log_id) +) engine=innodb comment = '定时任务调度日志表'; + + +-- ---------------------------- +-- 17、通知公告表 +-- ---------------------------- +drop table if exists sys_notice; +create table sys_notice ( + notice_id int(4) not null auto_increment comment '公告ID', + notice_title varchar(50) not null comment '公告标题', + notice_type char(1) not null comment '公告类型(1通知 2公告)', + notice_content longblob default null comment '公告内容', + status char(1) default '0' comment '公告状态(0正常 1关闭)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(255) default null comment '备注', + primary key (notice_id) +) engine=innodb auto_increment=10 comment = '通知公告表'; + +-- ---------------------------- +-- 初始化-公告信息表数据 +-- ---------------------------- +insert into sys_notice values('1', '温馨提醒:2018-07-01 若依新版本发布啦', '2', '新版本内容', '0', 'admin', sysdate(), '', null, '管理员'); +insert into sys_notice values('2', '维护通知:2018-07-01 若依系统凌晨维护', '1', '维护内容', '0', 'admin', sysdate(), '', null, '管理员'); + + +-- ---------------------------- +-- 18、代码生成业务表 +-- ---------------------------- +drop table if exists gen_table; +create table gen_table ( + table_id bigint(20) not null auto_increment comment '编号', + table_name varchar(200) default '' comment '表名称', + table_comment varchar(500) default '' comment '表描述', + sub_table_name varchar(64) default null comment '关联子表的表名', + sub_table_fk_name varchar(64) default null comment '子表关联的外键名', + class_name varchar(100) default '' comment '实体类名称', + tpl_category varchar(200) default 'crud' comment '使用的模板(crud单表操作 tree树表操作)', + package_name varchar(100) comment '生成包路径', + module_name varchar(30) comment '生成模块名', + business_name varchar(30) comment '生成业务名', + function_name varchar(50) comment '生成功能名', + function_author varchar(50) comment '生成功能作者', + gen_type char(1) default '0' comment '生成代码方式(0zip压缩包 1自定义路径)', + gen_path varchar(200) default '/' comment '生成路径(不填默认项目路径)', + options varchar(1000) comment '其它生成选项', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (table_id) +) engine=innodb auto_increment=1 comment = '代码生成业务表'; + + +-- ---------------------------- +-- 19、代码生成业务表字段 +-- ---------------------------- +drop table if exists gen_table_column; +create table gen_table_column ( + column_id bigint(20) not null auto_increment comment '编号', + table_id varchar(64) comment '归属表编号', + column_name varchar(200) comment '列名称', + column_comment varchar(500) comment '列描述', + column_type varchar(100) comment '列类型', + java_type varchar(500) comment 'JAVA类型', + java_field varchar(200) comment 'JAVA字段名', + is_pk char(1) comment '是否主键(1是)', + is_increment char(1) comment '是否自增(1是)', + is_required char(1) comment '是否必填(1是)', + is_insert char(1) comment '是否为插入字段(1是)', + is_edit char(1) comment '是否编辑字段(1是)', + is_list char(1) comment '是否列表字段(1是)', + is_query char(1) comment '是否查询字段(1是)', + query_type varchar(200) default 'EQ' comment '查询方式(等于、不等于、大于、小于、范围)', + html_type varchar(200) comment '显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)', + dict_type varchar(200) default '' comment '字典类型', + sort int comment '排序', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + primary key (column_id) +) engine=innodb auto_increment=1 comment = '代码生成业务表字段'; diff --git a/sql/test.sql b/sql/test.sql new file mode 100644 index 0000000..601360a --- /dev/null +++ b/sql/test.sql @@ -0,0 +1,171 @@ +DROP TABLE if EXISTS test_demo; +CREATE TABLE test_demo +( + id int(0) NOT NULL AUTO_INCREMENT COMMENT '主键', + dept_id int(0) NULL DEFAULT NULL COMMENT '部门id', + user_id int(0) NULL DEFAULT NULL COMMENT '用户id', + order_num int(0) NULL DEFAULT 0 COMMENT '排序号', + test_key varchar(255) NULL DEFAULT NULL COMMENT 'key键', + value varchar(255) NULL DEFAULT NULL COMMENT '值', + version int(0) NULL DEFAULT 0 COMMENT '版本', + create_time datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + create_by varchar(64) NULL DEFAULT NULL COMMENT '创建人', + update_time datetime(0) NULL DEFAULT NULL COMMENT '更新时间', + update_by varchar(64) NULL DEFAULT NULL COMMENT '更新人', + del_flag int(0) NULL DEFAULT 0 COMMENT '删除标志', + PRIMARY KEY (id) USING BTREE +) ENGINE = InnoDB COMMENT = '测试单表'; + +DROP TABLE if EXISTS test_tree; +CREATE TABLE test_tree +( + id int(0) NOT NULL AUTO_INCREMENT COMMENT '主键', + parent_id int(0) NULL DEFAULT 0 COMMENT '父id', + dept_id int(0) NULL DEFAULT NULL COMMENT '部门id', + user_id int(0) NULL DEFAULT NULL COMMENT '用户id', + tree_name varchar(255) NULL DEFAULT NULL COMMENT '值', + version int(0) NULL DEFAULT 0 COMMENT '版本', + create_time datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + create_by varchar(64) NULL DEFAULT NULL COMMENT '创建人', + update_time datetime(0) NULL DEFAULT NULL COMMENT '更新时间', + update_by varchar(64) NULL DEFAULT NULL COMMENT '更新人', + del_flag int(0) NULL DEFAULT 0 COMMENT '删除标志', + PRIMARY KEY (id) USING BTREE +) ENGINE = InnoDB COMMENT = '测试树表'; + +INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) VALUES (5, '测试菜单', 0, 5, 'demo', NULL, 1, 0, 'M', '0', '0', NULL, 'star', 'admin', '2021-05-30 00:34:26', NULL, NULL, ''); + +INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) VALUES (1500, '测试单表', 5, 1, 'demo', 'demo/demo/index', 1, 0, 'C', '0', '0', 'demo:demo:list', '#', 'admin', '2021-05-30 00:39:23', '', NULL, '测试单表菜单'); +INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) VALUES (1501, '测试单表查询', 1500, 1, '#', '', 1, 0, 'F', '0', '0', 'demo:demo:query', '#', 'admin', '2021-05-30 00:39:23', '', NULL, ''); +INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) VALUES (1502, '测试单表新增', 1500, 2, '#', '', 1, 0, 'F', '0', '0', 'demo:demo:add', '#', 'admin', '2021-05-30 00:39:23', '', NULL, ''); +INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) VALUES (1503, '测试单表修改', 1500, 3, '#', '', 1, 0, 'F', '0', '0', 'demo:demo:edit', '#', 'admin', '2021-05-30 00:39:23', '', NULL, ''); +INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) VALUES (1504, '测试单表删除', 1500, 4, '#', '', 1, 0, 'F', '0', '0', 'demo:demo:remove', '#', 'admin', '2021-05-30 00:39:23', '', NULL, ''); +INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) VALUES (1505, '测试单表导出', 1500, 5, '#', '', 1, 0, 'F', '0', '0', 'demo:demo:export', '#', 'admin', '2021-05-30 00:39:23', '', NULL, ''); + +INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) VALUES (1506, '测试树表', 5, 1, 'tree', 'demo/tree/index', 1, 0, 'C', '0', '0', 'demo:tree:list', '#', 'admin', '2021-05-30 00:39:30', '', NULL, '测试树表菜单'); +INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) VALUES (1507, '测试树表查询', 1506, 1, '#', '', 1, 0, 'F', '0', '0', 'demo:tree:query', '#', 'admin', '2021-05-30 00:39:30', '', NULL, ''); +INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) VALUES (1508, '测试树表新增', 1506, 2, '#', '', 1, 0, 'F', '0', '0', 'demo:tree:add', '#', 'admin', '2021-05-30 00:39:30', '', NULL, ''); +INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) VALUES (1509, '测试树表修改', 1506, 3, '#', '', 1, 0, 'F', '0', '0', 'demo:tree:edit', '#', 'admin', '2021-05-30 00:39:30', '', NULL, ''); +INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) VALUES (1510, '测试树表删除', 1506, 4, '#', '', 1, 0, 'F', '0', '0', 'demo:tree:remove', '#', 'admin', '2021-05-30 00:39:30', '', NULL, ''); +INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) VALUES (1511, '测试树表导出', 1506, 5, '#', '', 1, 0, 'F', '0', '0', 'demo:tree:export', '#', 'admin', '2021-05-30 00:39:30', '', NULL, ''); + +INSERT INTO sys_role(role_id, role_name, role_key, role_sort, data_scope, menu_check_strictly, dept_check_strictly, status, del_flag, create_by, create_time, update_by, update_time, remark) VALUES (3, '本部门及以下', 'test1', 3, '4', 1, 1, '0', '0', 'admin', '2021-05-08 22:31:37', 'admin', '2021-05-08 22:32:03', NULL); +INSERT INTO sys_role(role_id, role_name, role_key, role_sort, data_scope, menu_check_strictly, dept_check_strictly, status, del_flag, create_by, create_time, update_by, update_time, remark) VALUES (4, '仅本人', 'test2', 4, '5', 1, 1, '0', '0', 'admin', '2021-05-30 01:14:52', 'admin', '2021-05-30 01:18:38', NULL); + +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 5); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 100); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 101); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 102); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 103); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 104); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 105); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 106); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 107); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 108); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 500); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 501); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1001); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1002); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1003); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1004); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1005); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1006); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1007); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1008); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1009); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1010); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1011); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1012); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1013); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1014); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1015); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1016); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1017); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1018); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1019); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1020); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1021); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1022); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1023); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1024); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1025); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1026); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1027); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1028); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1029); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1030); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1031); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1032); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1033); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1034); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1035); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1036); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1037); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1038); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1039); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1040); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1041); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1042); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1043); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1044); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1045); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1500); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1501); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1502); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1503); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1504); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1505); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1506); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1507); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1508); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1509); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1510); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (3, 1511); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (4, 5); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (4, 1500); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (4, 1501); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (4, 1502); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (4, 1503); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (4, 1504); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (4, 1505); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (4, 1506); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (4, 1507); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (4, 1508); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (4, 1509); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (4, 1510); +INSERT INTO sys_role_menu(role_id, menu_id) VALUES (4, 1511); + +INSERT INTO sys_user(user_id, dept_id, user_name, nick_name, user_type, email, phonenumber, sex, avatar, password, status, del_flag, login_ip, login_date, create_by, create_time, update_by, update_time, remark) VALUES (3, 108, 'test', '本部门及以下 密码666', '00', '', '', '0', '', '$2a$10$M6tZRpUZbWKq11O/z6YISePQc./Jhru8E18mmVJTr9aV8whzfjacC', '0', '0', '127.0.0.1', '2021-05-30 02:00:37', 'admin', '2021-04-22 09:50:41', 'test', '2021-05-30 02:00:37', NULL); +INSERT INTO sys_user(user_id, dept_id, user_name, nick_name, user_type, email, phonenumber, sex, avatar, password, status, del_flag, login_ip, login_date, create_by, create_time, update_by, update_time, remark) VALUES (4, 102, 'test1', '仅本人 密码666', '00', '', '', '0', '', '$2a$10$yBSXp5Ba1m402cxXTPSy4eXUO8CXCGvXfquNVP/XMWwZ8nf9GaoMy', '0', '0', '127.0.0.1', '2021-05-30 01:48:03', 'admin', '2021-05-30 01:16:02', 'test1', '2021-05-30 01:48:03', NULL); + +INSERT INTO sys_user_role(user_id, role_id) VALUES (3, 3); +INSERT INTO sys_user_role(user_id, role_id) VALUES (4, 4); + +INSERT INTO test_demo(id, dept_id, user_id, order_num, test_key, value, version, create_time, create_by, update_time, update_by, del_flag) VALUES (1, 102, 4, 1, '测试数据权限', '测试', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_demo(id, dept_id, user_id, order_num, test_key, value, version, create_time, create_by, update_time, update_by, del_flag) VALUES (2, 102, 3, 2, '子节点1', '111', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_demo(id, dept_id, user_id, order_num, test_key, value, version, create_time, create_by, update_time, update_by, del_flag) VALUES (3, 102, 3, 3, '子节点2', '222', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_demo(id, dept_id, user_id, order_num, test_key, value, version, create_time, create_by, update_time, update_by, del_flag) VALUES (4, 108, 4, 4, '测试数据', 'demo', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_demo(id, dept_id, user_id, order_num, test_key, value, version, create_time, create_by, update_time, update_by, del_flag) VALUES (5, 108, 3, 13, '子节点11', '1111', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_demo(id, dept_id, user_id, order_num, test_key, value, version, create_time, create_by, update_time, update_by, del_flag) VALUES (6, 108, 3, 12, '子节点22', '2222', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_demo(id, dept_id, user_id, order_num, test_key, value, version, create_time, create_by, update_time, update_by, del_flag) VALUES (7, 108, 3, 11, '子节点33', '3333', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_demo(id, dept_id, user_id, order_num, test_key, value, version, create_time, create_by, update_time, update_by, del_flag) VALUES (8, 108, 3, 10, '子节点44', '4444', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_demo(id, dept_id, user_id, order_num, test_key, value, version, create_time, create_by, update_time, update_by, del_flag) VALUES (9, 108, 3, 9, '子节点55', '5555', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_demo(id, dept_id, user_id, order_num, test_key, value, version, create_time, create_by, update_time, update_by, del_flag) VALUES (10, 108, 3, 8, '子节点66', '6666', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_demo(id, dept_id, user_id, order_num, test_key, value, version, create_time, create_by, update_time, update_by, del_flag) VALUES (11, 108, 3, 7, '子节点77', '7777', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_demo(id, dept_id, user_id, order_num, test_key, value, version, create_time, create_by, update_time, update_by, del_flag) VALUES (12, 108, 3, 6, '子节点88', '8888', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_demo(id, dept_id, user_id, order_num, test_key, value, version, create_time, create_by, update_time, update_by, del_flag) VALUES (13, 108, 3, 5, '子节点99', '9999', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); + +INSERT INTO test_tree(id, parent_id, dept_id, user_id, tree_name, version, create_time, create_by, update_time, update_by, del_flag) VALUES (1, 0, 102, 4, '测试数据权限', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_tree(id, parent_id, dept_id, user_id, tree_name, version, create_time, create_by, update_time, update_by, del_flag) VALUES (2, 1, 102, 3, '子节点1', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_tree(id, parent_id, dept_id, user_id, tree_name, version, create_time, create_by, update_time, update_by, del_flag) VALUES (3, 2, 102, 3, '子节点2', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_tree(id, parent_id, dept_id, user_id, tree_name, version, create_time, create_by, update_time, update_by, del_flag) VALUES (4, 0, 108, 4, '测试树1', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_tree(id, parent_id, dept_id, user_id, tree_name, version, create_time, create_by, update_time, update_by, del_flag) VALUES (5, 4, 108, 3, '子节点11', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_tree(id, parent_id, dept_id, user_id, tree_name, version, create_time, create_by, update_time, update_by, del_flag) VALUES (6, 4, 108, 3, '子节点22', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_tree(id, parent_id, dept_id, user_id, tree_name, version, create_time, create_by, update_time, update_by, del_flag) VALUES (7, 4, 108, 3, '子节点33', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_tree(id, parent_id, dept_id, user_id, tree_name, version, create_time, create_by, update_time, update_by, del_flag) VALUES (8, 5, 108, 3, '子节点44', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_tree(id, parent_id, dept_id, user_id, tree_name, version, create_time, create_by, update_time, update_by, del_flag) VALUES (9, 6, 108, 3, '子节点55', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_tree(id, parent_id, dept_id, user_id, tree_name, version, create_time, create_by, update_time, update_by, del_flag) VALUES (10, 7, 108, 3, '子节点66', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_tree(id, parent_id, dept_id, user_id, tree_name, version, create_time, create_by, update_time, update_by, del_flag) VALUES (11, 7, 108, 3, '子节点77', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_tree(id, parent_id, dept_id, user_id, tree_name, version, create_time, create_by, update_time, update_by, del_flag) VALUES (12, 10, 108, 3, '子节点88', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); +INSERT INTO test_tree(id, parent_id, dept_id, user_id, tree_name, version, create_time, create_by, update_time, update_by, del_flag) VALUES (13, 10, 108, 3, '子节点99', 0, '2021-06-01 10:00:00', 'admin', NULL, NULL, 0); diff --git a/up.sql b/up.sql new file mode 100644 index 0000000..3b1f3f2 --- /dev/null +++ b/up.sql @@ -0,0 +1,7 @@ +ALTER TABLE `dk_customer_info` + ADD COLUMN `income_wan` decimal(10, 2) NULL; +ALTER TABLE `dk_borrow` + ADD COLUMN `income_wan` decimal(10, 2) NULL; + +ALTER TABLE `dk_agreement_setting` + ADD COLUMN `contract_template` text NULL;