From e916d067f3fac7a14655d277f33859812f4bcdc9 Mon Sep 17 00:00:00 2001 From: ZhouXY108 Date: Wed, 7 Dec 2022 18:14:38 +0800 Subject: [PATCH] first commit. --- .editorconfig | 36 + .gitignore | 34 + README.md | 21 + err_code.json | 774 ++++++++++++++++++ .../plusone-basic-application/pom.xml | 34 + .../config/PlusoneExceptionHandlerConfig.java | 17 + .../handler/AllExceptionHandler.java | 29 + .../handler/DefaultExceptionHandler.java | 63 ++ plusone-basic/plusone-basic-common/pom.xml | 46 ++ .../exception/DataNotExistException.java | 31 + .../DataOperationNumberException.java | 31 + .../exception/InvalidInputException.java | 56 ++ .../exception/UserOperationException.java | 58 ++ .../xyz/zhouxy/plusone/util/AssertResult.java | 67 ++ .../zhouxy/plusone/util/PlusoneStrUtil.java | 43 + plusone-basic/plusone-basic-domain/pom.xml | 39 + .../zhouxy/plusone/constant/EntityStatus.java | 32 + .../zhouxy/plusone/constant/RegexConsts.java | 27 + .../zhouxy/plusone/domain/AggregateRoot.java | 17 + .../zhouxy/plusone/domain/DomainEvent.java | 26 + .../xyz/zhouxy/plusone/domain/Entity.java | 32 + .../xyz/zhouxy/plusone/domain/ICommand.java | 10 + .../plusone/domain/ICommandHandler.java | 12 + .../zhouxy/plusone/domain/IEventHandler.java | 12 + .../zhouxy/plusone/domain/IRepository.java | 21 + .../zhouxy/plusone/domain/IValueObject.java | 9 + .../xyz/zhouxy/plusone/domain/IWithLabel.java | 10 + .../plusone/domain/IWithOrderNumber.java | 33 + .../zhouxy/plusone/domain/IWithVersion.java | 10 + .../domain/ValidatableStringRecord.java | 34 + .../xyz/zhouxy/plusone/util/RegexUtil.java | 36 + .../plusone-basic-infrastructure/pom.xml | 95 +++ .../zhouxy/plusone/config/PlusoneConfig.java | 14 + .../infrastructure/pojo/AbstractEntityPO.java | 41 + .../plusone/jdbc/BeanPropertyParamSource.java | 33 + .../xyz/zhouxy/plusone/jdbc/JdbcConfig.java | 51 ++ .../plusone/jdbc/JdbcEntityDaoSupport.java | 68 ++ .../xyz/zhouxy/plusone/jdbc/JdbcFactory.java | 30 + .../plusone/jdbc/JdbcRepositorySupport.java | 58 ++ .../plusone/jdbc/common/ResultMapper.java | 45 + .../jdbc/common/SimpleResultMapper.java | 30 + .../converter/EnumToOrdinalConverter.java | 20 + .../converter/OrdinalToEnumConverter.java | 44 + .../plusone/mail/MailMessageFactory.java | 31 + .../xyz/zhouxy/plusone/mail/MailService.java | 17 + .../mail/PlusoneMailAutoConfiguration.java | 26 + .../plusone/mail/PlusoneMailProperties.java | 16 + .../plusone/mail/SimpleMailService.java | 57 ++ .../mybatis/MyBatisAutoConfiguration.java | 17 + .../plusone/mybatis/MyBatisPlusConfig.java | 19 + .../zhouxy/plusone/mybatis/MybatisUtil.java | 28 + .../plusone/redis/RedisStrCacheDAO.java | 42 + .../xyz/zhouxy/plusone/redis/StrCacheDAO.java | 17 + .../sms/PlusoneSmsAutoConfiguration.java | 27 + .../xyz/zhouxy/plusone/sms/SmsProperties.java | 51 ++ .../xyz/zhouxy/plusone/sms/SmsService.java | 13 + .../plusone/sms/TencentSmsServiceImpl.java | 145 ++++ .../plusone/spring/SpringContextHolder.java | 28 + .../main/java/xyz/zhouxy/plusone/sql/SQL.java | 34 + .../plusone/validator/BaseValidator.java | 72 ++ .../plusone/validator/BaseValidator2.java | 129 +++ .../plusone/validator/DtoValidator.java | 12 + .../plusone/validator/IValidateRequired.java | 13 + .../zhouxy/plusone/validator/ValidateDto.java | 42 + .../plusone/validator/ValidateUtil.java | 29 + .../zhouxy/plusone/validator/Validator.java | 42 + .../plusone/web/config/WebCorsConfig.java | 23 + .../plusone/web/config/WebEnumMapConfig.java | 55 ++ .../main/resources/conf/plusone.properties | 31 + .../plusone/validator/BaseValidator2Test.java | 41 + plusone-basic/pom.xml | 57 ++ plusone-start/pom.xml | 85 ++ .../zhouxy/plusone/PlusoneApplication.java | 17 + ...itional-spring-configuration-metadata.json | 34 + .../src/main/resources/application.yaml | 79 ++ plusone-start/src/main/resources/logback.xml | 32 + .../java/xyz/zhouxy/plusone/SerialTests.java | 22 + .../test/java/xyz/zhouxy/plusone/TestAop.java | 31 + .../plusone-system-application/pom.xml | 27 + .../application/common/util/MenuUtil.java | 61 ++ .../common/util/PrincipalType.java | 18 + .../common/util/PrincipalUtil.java | 64 ++ .../controller/AccountContextController.java | 40 + .../AccountManagementController.java | 83 ++ .../controller/AdminLoginController.java | 49 ++ .../controller/AdminLogoutController.java | 30 + .../controller/DictManagementController.java | 83 ++ .../controller/MenuManagementController.java | 85 ++ .../controller/RegisterAccountController.java | 42 + .../controller/RoleManagementController.java | 78 ++ .../exception/AccountLoginException.java | 38 + .../exception/AccountRegisterException.java | 57 ++ .../UnsupportedMenuTypeException.java | 17 + .../handler/SaTokenExceptionHandler.java | 56 ++ .../application/query/AccountQueries.java | 32 + .../system/application/query/DictQueries.java | 39 + .../application/query/PermissionQueries.java | 10 + .../system/application/query/RoleQueries.java | 64 ++ .../query/params/AccountQueryParams.java | 55 ++ .../query/params/DictQueryParams.java | 21 + .../query/params/RoleQueryParams.java | 34 + .../query/result/AccountDetails.java | 29 + .../query/result/AccountOverview.java | 38 + .../query/result/DictOverview.java | 22 + .../query/result/LoginInfoViewObject.java | 24 + .../query/result/MenuViewObject.java | 131 +++ .../query/result/RoleOverview.java | 23 + .../service/AccountContextService.java | 38 + .../service/AccountManagementService.java | 105 +++ .../service/AdminLoginService.java | 104 +++ .../service/AdminLogoutService.java | 19 + .../application/service/AuthService.java | 60 ++ .../service/DictManagementService.java | 74 ++ .../service/MailAndSmsVerifyService.java | 113 +++ .../service/MenuManagementService.java | 174 ++++ .../service/RegisterAccountService.java | 98 +++ .../service/RoleManagementService.java | 95 +++ .../service/command/CreateAccountCommand.java | 56 ++ .../service/command/CreateDictCommand.java | 27 + .../service/command/CreateMenuCommand.java | 55 ++ .../service/command/CreateRoleCommand.java | 31 + .../service/command/LoginByOtpCommand.java | 21 + .../command/LoginByPasswordCommand.java | 21 + .../command/RegisterAccountCommand.java | 45 + .../service/command/UpdateAccountCommand.java | 31 + .../service/command/UpdateDictCommand.java | 30 + .../service/command/UpdateMenuCommand.java | 59 ++ .../service/command/UpdateRoleCommand.java | 40 + .../validator/LoginByOtpCommandValidator.java | 29 + .../LoginByPasswordCommandValidator.java | 31 + .../application/query/AccountQueries.xml | 196 +++++ plusone-system/plusone-system-common/pom.xml | 23 + .../plusone/system/constant/AuthLogic.java | 20 + .../plusone/system/util/PasswordUtil.java | 51 ++ plusone-system/plusone-system-domain/pom.xml | 23 + .../system/domain/event/AccountCreated.java | 33 + .../system/domain/event/AccountLocked.java | 33 + .../domain/event/AccountPasswordChanged.java | 34 + .../system/domain/event/AccountRecovered.java | 27 + .../domain/event/AccountRolesBound.java | 27 + .../system/domain/event/EmailChanged.java | 30 + .../domain/event/MobilePhoneChanged.java | 30 + .../system/domain/event/UsernameChanged.java | 33 + .../system/domain/model/account/Account.java | 235 ++++++ .../domain/model/account/AccountInfo.java | 43 + .../model/account/AccountRepository.java | 29 + .../domain/model/account/AccountStatus.java | 29 + .../system/domain/model/account/Email.java | 47 ++ .../domain/model/account/MobilePhone.java | 44 + .../system/domain/model/account/Nickname.java | 35 + .../system/domain/model/account/Password.java | 90 ++ .../domain/model/account/Principal.java | 14 + .../system/domain/model/account/Sex.java | 30 + .../system/domain/model/account/Username.java | 28 + .../system/domain/model/dict/Dict.java | 129 +++ .../domain/model/dict/DictRepository.java | 10 + .../system/domain/model/dict/DictValue.java | 51 ++ .../system/domain/model/menu/Action.java | 54 ++ .../system/domain/model/menu/Menu.java | 140 ++++ .../domain/model/menu/MenuConstructor.java | 55 ++ .../domain/model/menu/MenuRepository.java | 15 + .../system/domain/model/menu/Target.java | 26 + .../domain/model/permission/Action.java | 47 ++ .../domain/model/permission/Permission.java | 69 ++ .../system/domain/model/permission/README.md | 3 + .../system/domain/model/role/ActionRef.java | 12 + .../system/domain/model/role/MenuRef.java | 12 + .../system/domain/model/role/Role.java | 112 +++ .../domain/model/role/RoleRepository.java | 11 + .../system/domain/service/MenuService.java | 44 + .../domain/model/account/PasswordTests.java | 18 + .../plusone-system-infrastructure/pom.xml | 23 + .../model/account/AccountRepositoryImpl.java | 218 +++++ .../model/account/AccountRoleRefDAO.java | 53 ++ .../domain/model/dict/DictRepositoryImpl.java | 118 +++ .../domain/model/dict/DictValueDAO.java | 51 ++ .../system/domain/model/menu/ActionDAO.java | 123 +++ .../domain/model/menu/MenuRepositoryImpl.java | 200 +++++ .../domain/model/role/RoleMenuRefDAO.java | 45 + .../model/role/RolePermissionRefDAO.java | 45 + .../domain/model/role/RoleRepositoryImpl.java | 142 ++++ plusone-system/pom.xml | 79 ++ pom.xml | 211 +++++ 183 files changed, 9649 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 README.md create mode 100644 err_code.json create mode 100644 plusone-basic/plusone-basic-application/pom.xml create mode 100644 plusone-basic/plusone-basic-application/src/main/java/xyz/zhouxy/plusone/exception/config/PlusoneExceptionHandlerConfig.java create mode 100644 plusone-basic/plusone-basic-application/src/main/java/xyz/zhouxy/plusone/exception/handler/AllExceptionHandler.java create mode 100644 plusone-basic/plusone-basic-application/src/main/java/xyz/zhouxy/plusone/exception/handler/DefaultExceptionHandler.java create mode 100644 plusone-basic/plusone-basic-common/pom.xml create mode 100644 plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/exception/DataNotExistException.java create mode 100644 plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/exception/DataOperationNumberException.java create mode 100644 plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/exception/InvalidInputException.java create mode 100644 plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/exception/UserOperationException.java create mode 100644 plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/util/AssertResult.java create mode 100644 plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/util/PlusoneStrUtil.java create mode 100644 plusone-basic/plusone-basic-domain/pom.xml create mode 100644 plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/constant/EntityStatus.java create mode 100644 plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/constant/RegexConsts.java create mode 100644 plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/AggregateRoot.java create mode 100644 plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/DomainEvent.java create mode 100644 plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/Entity.java create mode 100644 plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/ICommand.java create mode 100644 plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/ICommandHandler.java create mode 100644 plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IEventHandler.java create mode 100644 plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IRepository.java create mode 100644 plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IValueObject.java create mode 100644 plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IWithLabel.java create mode 100644 plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IWithOrderNumber.java create mode 100644 plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IWithVersion.java create mode 100644 plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/ValidatableStringRecord.java create mode 100644 plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/util/RegexUtil.java create mode 100644 plusone-basic/plusone-basic-infrastructure/pom.xml create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/config/PlusoneConfig.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/infrastructure/pojo/AbstractEntityPO.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/BeanPropertyParamSource.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/JdbcConfig.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/JdbcEntityDaoSupport.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/JdbcFactory.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/JdbcRepositorySupport.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/common/ResultMapper.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/common/SimpleResultMapper.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/converter/EnumToOrdinalConverter.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/converter/OrdinalToEnumConverter.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mail/MailMessageFactory.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mail/MailService.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mail/PlusoneMailAutoConfiguration.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mail/PlusoneMailProperties.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mail/SimpleMailService.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mybatis/MyBatisAutoConfiguration.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mybatis/MyBatisPlusConfig.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mybatis/MybatisUtil.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/redis/RedisStrCacheDAO.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/redis/StrCacheDAO.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/sms/PlusoneSmsAutoConfiguration.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/sms/SmsProperties.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/sms/SmsService.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/sms/TencentSmsServiceImpl.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/spring/SpringContextHolder.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/sql/SQL.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/BaseValidator.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/BaseValidator2.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/DtoValidator.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/IValidateRequired.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/ValidateDto.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/ValidateUtil.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/Validator.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/web/config/WebCorsConfig.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/web/config/WebEnumMapConfig.java create mode 100644 plusone-basic/plusone-basic-infrastructure/src/main/resources/conf/plusone.properties create mode 100644 plusone-basic/plusone-basic-infrastructure/src/test/java/xyz/zhouxy/plusone/validator/BaseValidator2Test.java create mode 100644 plusone-basic/pom.xml create mode 100644 plusone-start/pom.xml create mode 100644 plusone-start/src/main/java/xyz/zhouxy/plusone/PlusoneApplication.java create mode 100644 plusone-start/src/main/resources/META-INF/additional-spring-configuration-metadata.json create mode 100644 plusone-start/src/main/resources/application.yaml create mode 100644 plusone-start/src/main/resources/logback.xml create mode 100644 plusone-start/src/test/java/xyz/zhouxy/plusone/SerialTests.java create mode 100644 plusone-start/src/test/java/xyz/zhouxy/plusone/TestAop.java create mode 100644 plusone-system/plusone-system-application/pom.xml create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/common/util/MenuUtil.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/common/util/PrincipalType.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/common/util/PrincipalUtil.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/AccountContextController.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/AccountManagementController.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/AdminLoginController.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/AdminLogoutController.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/DictManagementController.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/MenuManagementController.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/RegisterAccountController.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/RoleManagementController.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/exception/AccountLoginException.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/exception/AccountRegisterException.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/exception/UnsupportedMenuTypeException.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/exception/handler/SaTokenExceptionHandler.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/AccountQueries.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/DictQueries.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/PermissionQueries.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/RoleQueries.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/params/AccountQueryParams.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/params/DictQueryParams.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/params/RoleQueryParams.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/AccountDetails.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/AccountOverview.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/DictOverview.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/LoginInfoViewObject.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/MenuViewObject.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/RoleOverview.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/AccountContextService.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/AccountManagementService.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/AdminLoginService.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/AdminLogoutService.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/AuthService.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/DictManagementService.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/MailAndSmsVerifyService.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/MenuManagementService.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/RegisterAccountService.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/RoleManagementService.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/CreateAccountCommand.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/CreateDictCommand.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/CreateMenuCommand.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/CreateRoleCommand.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/LoginByOtpCommand.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/LoginByPasswordCommand.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/RegisterAccountCommand.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/UpdateAccountCommand.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/UpdateDictCommand.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/UpdateMenuCommand.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/UpdateRoleCommand.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/validator/LoginByOtpCommandValidator.java create mode 100644 plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/validator/LoginByPasswordCommandValidator.java create mode 100644 plusone-system/plusone-system-application/src/main/resources/xyz/zhouxy/plusone/system/application/query/AccountQueries.xml create mode 100644 plusone-system/plusone-system-common/pom.xml create mode 100644 plusone-system/plusone-system-common/src/main/java/xyz/zhouxy/plusone/system/constant/AuthLogic.java create mode 100644 plusone-system/plusone-system-common/src/main/java/xyz/zhouxy/plusone/system/util/PasswordUtil.java create mode 100644 plusone-system/plusone-system-domain/pom.xml create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/AccountCreated.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/AccountLocked.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/AccountPasswordChanged.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/AccountRecovered.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/AccountRolesBound.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/EmailChanged.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/MobilePhoneChanged.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/UsernameChanged.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Account.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/AccountInfo.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/AccountRepository.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/AccountStatus.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Email.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/MobilePhone.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Nickname.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Password.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Principal.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Sex.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Username.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/dict/Dict.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/dict/DictRepository.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/dict/DictValue.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/Action.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/Menu.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/MenuConstructor.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/MenuRepository.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/Target.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/permission/Action.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/permission/Permission.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/permission/README.md create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/ActionRef.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/MenuRef.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/Role.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/RoleRepository.java create mode 100644 plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/service/MenuService.java create mode 100644 plusone-system/plusone-system-domain/src/test/java/xyz/zhouxy/plusone/system/domain/model/account/PasswordTests.java create mode 100644 plusone-system/plusone-system-infrastructure/pom.xml create mode 100644 plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/AccountRepositoryImpl.java create mode 100644 plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/AccountRoleRefDAO.java create mode 100644 plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/dict/DictRepositoryImpl.java create mode 100644 plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/dict/DictValueDAO.java create mode 100644 plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/ActionDAO.java create mode 100644 plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/MenuRepositoryImpl.java create mode 100644 plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/RoleMenuRefDAO.java create mode 100644 plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/RolePermissionRefDAO.java create mode 100644 plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/RoleRepositoryImpl.java create mode 100644 plusone-system/pom.xml create mode 100644 pom.xml diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d37d8e9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,36 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + + +[{*.ng,*.sht,*.html,*.shtm,*.shtml,*.htm}] +indent_size=4 + +[{*.jhm,*.xslt,*.xul,*.rng,*.xsl,*.xsd,*.ant,*.tld,*.fxml,*.jrxml,*.xml,*.jnlp,*.wsdl}] +indent_size=4 + +[{.babelrc,.stylelintrc,jest.config,.eslintrc,.prettierrc,*.json,*.jsb3,*.jsb2,*.bowerrc}] +indent_size=2 + +[*.svg] +indent_size=2 + +[*.js.map] +indent_size=2 + +[*.less] +indent_size=2 + +[*.vue] +indent_size=4 + +[{.analysis_options,*.yml,*.yaml}] +indent_size=2 + +[*.java] +indent_size = 4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96041e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ +*/**/http/*.http + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..1dfb6fd --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# PlusoneAdmin + +后台管理系统 +1 + +本仓库是后端部分,使用 Spring Boot 2.X 进行开发,对前端提供接口。 + +简单落地 DDD。 + +- JDK 17 + +- 主要使用 Spring JDBC 的 NamedParameterJdbcTemplate 进行数据的查询和持久化,因为感觉有更多的自由度。可以根据 ResultSet,任意按照需要的方式实例化 Entity。MyBatis 在某些地方用于查询。 + +- 数据库使用的是 PostgreSQL; + +- 权限管理使用的是 Sa-Token,使用 Redis 共享 Session。 + +- 短信服务使用的是腾讯云的 SMS 服务。后续将其解耦,使实现等各方面可替换为阿里云或其它 SMS 服务。 + +目前项目还没完成,开发中…… + +相关的文档和介绍完善中…… \ No newline at end of file diff --git a/err_code.json b/err_code.json new file mode 100644 index 0000000..0752942 --- /dev/null +++ b/err_code.json @@ -0,0 +1,774 @@ +[ + { + "code": 2000000, + "description": "正常" + }, + { + "code": 4000000, + "description": "用户端错误", + "children": [ + { + "code": 4010000, + "description": "用户注册错误", + "children": [ + { + "code": 4010100, + "description": "用户未同意隐私协议" + }, + { + "code": 4010200, + "description": "注册国家或地区受限" + }, + { + "code": 4010300, + "description": "邮箱和手机号应至少绑定一个" + }, + { + "code": 4010400, + "description": "用户名已存在" + }, + { + "code": 4010500, + "description": "邮箱已存在" + }, + { + "code": 4010600, + "description": "手机号已存在" + }, + { + "code": 4010700, + "description": "校验码异常", + "children": [ + { + "code": 4010701, + "description": "校验码输入错误" + }, + { + "code": 4010702, + "description": "校验码不存在或过期" + } + ] + }, + { + "code": 4010800, + "description": "用户证件异常", + "children": [ + { + "code": 4010801, + "description": "用户证件类型未选择" + }, + { + "code": 4010802, + "description": "大陆身份证编号校验非法" + }, + { + "code": 4010803, + "description": "护照编号校验非法" + }, + { + "code": 4010804, + "description": "军官证编号校验非法" + } + ] + } + ] + }, + { + "code": 4020000, + "description": "用户登录异常", + "children": [ + { + "code": 4020100, + "description": "用户账户异常", + "children": [ + { + "code": 4020101, + "description": "用户账户不存在" + }, + { + "code": 4020102, + "description": "用户账户被冻结" + }, + { + "code": 4020103, + "description": "用户账户已作废" + } + ] + }, + { + "code": 4020200, + "description": "用户密码错误", + "children": [ + { + "code": 4020201, + "description": "用户输入密码错误次数超限" + } + ] + }, + { + "code": 4020300, + "description": "用户身份校验失败", + "children": [ + { + "code": 4020301, + "description": "用户指纹识别失败" + }, + { + "code": 4020302, + "description": "用户面容识别失败" + }, + { + "code": 4020303, + "description": "用户未获得第三方登录授权" + } + ] + }, + { + "code": 4020400, + "description": "用户登录已过期" + }, + { + "code": 4020500, + "description": "用户验证码错误", + "children": [ + { + "code": 4020501, + "description": "用户验证码输入错误" + }, + { + "code": 4020502, + "description": "用户验证码不存在或已过期" + } + ] + } + ] + }, + { + "code": 4030000, + "description": "访问权限异常", + "children": [ + { + "code": 4030100, + "description": "访问未授权", + "children": [ + { + "code": 4030101, + "description": "因访问对象隐私设置被拦截" + }, + { + "code": 4030102, + "description": "授权已过期" + }, + { + "code": 4030103, + "description": "无权限使用 API" + } + ] + }, + { + "code": 4030200, + "description": "用户访问被拦截", + "children": [ + { + "code": 4030201, + "description": "黑名单用户" + }, + { + "code": 4030202, + "description": "账号被冻结" + }, + { + "code": 4030203, + "description": "非法 IP 地址" + }, + { + "code": 4030204, + "description": "网关访问受限" + }, + { + "code": 4030205, + "description": "地域黑名单" + } + ] + }, + { + "code": 4030300, + "description": "服务已欠费" + }, + { + "code": 4030400, + "description": "用户签名异常", + "children": [ + { + "code": 4030401, + "description": "RSA 签名错误" + } + ] + } + ] + }, + { + "code": 4040000, + "description": "用户请求参数错误", + "children": [ + { + "code": 4040100, + "description": "包含非法恶意跳转链接" + }, + { + "code": 4040200, + "description": "无效的用户输入", + "children": [ + { + "code": 4040201, + "description": "不支持的 PrincipalType" + } + ] + }, + { + "code": 4040300, + "description": "请求必填参数为空", + "children": [ + { + "code": 4040301, + "description": "用户订单号为空" + }, + { + "code": 4040302, + "description": "订购数量为空" + }, + { + "code": 4040303, + "description": "缺少时间戳参数" + }, + { + "code": 4040304, + "description": "非法的时间戳参数" + } + ] + }, + { + "code": 4040400, + "description": "请求参数值超出允许的范围", + "children": [ + { + "code": 4040401, + "description": "参数格式不匹配" + }, + { + "code": 4040402, + "description": "地址不在服务范围" + }, + { + "code": 4040403, + "description": "时间不在服务范围" + }, + { + "code": 4040404, + "description": "金额超出限制" + }, + { + "code": 4040405, + "description": "数量超出限制" + }, + { + "code": 4040406, + "description": "请求批量处理总个数超出限制" + }, + { + "code": 4040407, + "description": "请求 JSON 解析失败" + } + ] + }, + { + "code": 4040500, + "description": "用户输入内容非法", + "children": [ + { + "code": 4040501, + "description": "包含违禁敏感词" + }, + { + "code": 4040502, + "description": "图片包含违禁信息" + }, + { + "code": 4040503, + "description": "文件侵犯版权" + } + ] + }, + { + "code": 4040600, + "description": "用户操作异常", + "children": [ + { + "code": 4040601, + "description": "用户支付超时" + }, + { + "code": 4040602, + "description": "确认订单超时" + }, + { + "code": 4040603, + "description": "订单已关闭" + } + ] + } + ] + }, + { + "code": 4050000, + "description": "用户请求服务异常", + "children": [ + { + "code": 4050100, + "description": "请求次数超出限制" + }, + { + "code": 4050200, + "description": "请求并发数超出限制" + }, + { + "code": 4050300, + "description": "用户操作请等待" + }, + { + "code": 4050400, + "description": "WebSocket 连接异常" + }, + { + "code": 4050500, + "description": "WebSocket 连接断开" + }, + { + "code": 4050600, + "description": "用户重复请求" + } + ] + }, + { + "code": 4060000, + "description": "用户资源异常", + "children": [ + { + "code": 4060100, + "description": "账户余额不足" + }, + { + "code": 4060200, + "description": "用户磁盘空间不足" + }, + { + "code": 4060300, + "description": "用户内存空间不足" + }, + { + "code": 4060400, + "description": "用户 OSS 容量不足" + }, + { + "code": 4060500, + "description": "用户配额已用光" + } + ] + }, + { + "code": 4070000, + "description": "用户上传文件异常", + "children": [ + { + "code": 4070100, + "description": "用户上传文件类型不匹配" + }, + { + "code": 4070200, + "description": "用户上传文件太大" + }, + { + "code": 4070300, + "description": "用户上传图片太大" + }, + { + "code": 4070400, + "description": "用户上传视频太大" + }, + { + "code": 4070500, + "description": "用户上传压缩文件太大" + } + ] + }, + { + "code": 4080000, + "description": "用户当前版本异常", + "children": [ + { + "code": 4080100, + "description": "用户安装版本与系统不匹配", + "children": [ + { + "code": 4080101, + "description": "用户安装版本过低" + }, + { + "code": 4080102, + "description": "用户安装版本过高" + }, + { + "code": 4080103, + "description": "用户安装版本已过期" + } + ] + }, + { + "code": 4080200, + "description": "用户 API 请求版本不匹配", + "children": [ + { + "code": 4080201, + "description": "用户 API 请求版本过高" + }, + { + "code": 4080202, + "description": "用户 API 请求版本过低" + } + ] + } + ] + }, + { + "code": 4090000, + "description": "用户隐私未授权", + "children": [ + { + "code": 4090100, + "description": "用户隐私未签署" + }, + { + "code": 4090200, + "description": "用户摄像头未授权" + }, + { + "code": 4090300, + "description": "用户相机未授权" + }, + { + "code": 4090400, + "description": "用户图片库未授权" + }, + { + "code": 4090500, + "description": "用户文件未授权" + }, + { + "code": 4090600, + "description": "用户位置信息未授权" + }, + { + "code": 4090700, + "description": "用户通讯录未授权" + } + ] + }, + { + "code": 4100000, + "description": "用户设备异常", + "children": [ + { + "code": 4100100, + "description": "用户相机异常" + }, + { + "code": 4100200, + "description": "用户麦克风异常" + }, + { + "code": 4100300, + "description": "用户听筒异常" + }, + { + "code": 4100400, + "description": "用户扬声器异常" + }, + { + "code": 4100500, + "description": "用户 GPS 定位异常" + } + ] + }, + { + "code": 4110000, + "description": "数据资源异常", + "children": [ + { + "code": 4110100, + "description": "数据资源不存在" + }, + { + "code": 4110200, + "description": "数据操作的行数不符合预期" + } + ] + } + ] + }, + { + "code": 5000000, + "description": "系统执行出错", + "children": [ + { + "code": 5010000, + "description": "系统执行超时", + "children": [ + { + "code": 5010100, + "description": "系统订单处理超时" + } + ] + }, + { + "code": 5020000, + "description": "系统容灾功能被触发", + "children": [ + { + "code": 5020100, + "description": "系统限流" + }, + { + "code": 5020200, + "description": "系统功能降级" + } + ] + }, + { + "code": 5030000, + "description": "系统资源异常", + "children": [ + { + "code": 5030100, + "description": "系统资源耗尽", + "children": [ + { + "code": 5030101, + "description": "系统磁盘空间耗尽" + }, + { + "code": 5030102, + "description": "系统内存耗尽" + }, + { + "code": 5030103, + "description": "文件句柄耗尽" + }, + { + "code": 5030104, + "description": "系统连接池耗尽" + }, + { + "code": 5030105, + "description": "系统线程池耗尽" + } + ] + }, + { + "code": 5030200, + "description": "系统资源访问异常", + "children": [ + { + "code": 5030201, + "description": "系统读取磁盘文件失败" + } + ] + } + ] + } + ] + }, + { + "code": 6000000, + "description": "调用第三方服务出错", + "children": [ + { + "code": 6010000, + "description": "中间件服务出错", + "children": [ + { + "code": 6010100, + "description": "RPC 服务出错", + "children": [ + { + "code": 6010101, + "description": "RPC 服务未找到" + }, + { + "code": 6010102, + "description": "RPC 服务未注册" + }, + { + "code": 6010103, + "description": "接口不存在" + } + ] + }, + { + "code": 6010200, + "description": "消息服务出错", + "children": [ + { + "code": 6010201, + "description": "消息投递出错" + }, + { + "code": 6010202, + "description": "消息消费出错" + }, + { + "code": 6010203, + "description": "消息订阅出错" + }, + { + "code": 6010204, + "description": "消息分组未查到" + } + ] + }, + { + "code": 6010300, + "description": "缓存服务出错", + "children": [ + { + "code": 6010301, + "description": "key 长度超过限制" + }, + { + "code": 6010302, + "description": "value 长度超过限制" + }, + { + "code": 6010303, + "description": "存储容量已满" + }, + { + "code": 6010304, + "description": "不支持的数据格式" + } + ] + }, + { + "code": 6010400, + "description": "配置服务出错" + }, + { + "code": 6010500, + "description": "网络资源服务出错", + "children": [ + { + "code": 6010501, + "description": "VPN 服务出错" + }, + { + "code": 6010502, + "description": "CDN 服务出错" + }, + { + "code": 6010503, + "description": "域名解析服务出错" + }, + { + "code": 6010504, + "description": "网关服务出错" + } + ] + } + ] + }, + { + "code": 6020000, + "description": "第三方系统执行超时", + "children": [ + { + "code": 6020100, + "description": "RPC 执行超时" + }, + { + "code": 6020200, + "description": "消息投递超时" + }, + { + "code": 6020300, + "description": "缓存服务超时" + }, + { + "code": 6020400, + "description": "配置服务超时" + }, + { + "code": 6020500, + "description": "数据库服务超时" + } + ] + }, + { + "code": 6030000, + "description": "数据库服务出错", + "children": [ + { + "code": 6030100, + "description": "表不存在" + }, + { + "code": 6030200, + "description": "列不存在" + }, + { + "code": 6030300, + "description": "多表关联中存在多个相同名称的列" + }, + { + "code": 6030400, + "description": "数据库死锁" + }, + { + "code": 6030500, + "description": "主键冲突" + } + ] + }, + { + "code": 6040000, + "description": "第三方容灾系统被触发", + "children": [ + { + "code": 6040100, + "description": "第三方系统限流" + }, + { + "code": 6040200, + "description": "第三方功能降级" + } + ] + }, + { + "code": 6050000, + "description": "通知服务出错", + "children": [ + { + "code": 6050100, + "description": "短信提醒服务失败" + }, + { + "code": 6050200, + "description": "语音提醒服务失败" + }, + { + "code": 6050300, + "description": "邮件提醒服务失败" + } + ] + } + ] + } +] diff --git a/plusone-basic/plusone-basic-application/pom.xml b/plusone-basic/plusone-basic-application/pom.xml new file mode 100644 index 0000000..21baad0 --- /dev/null +++ b/plusone-basic/plusone-basic-application/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + plusone-basic + xyz.zhouxy + 1.0.0-SNAPSHOT + ../pom.xml + + + plusone-basic-application + + + + xyz.zhouxy + plusone-basic-common + + + xyz.zhouxy + plusone-basic-domain + + + xyz.zhouxy + plusone-basic-infrastructure + + + + + net.dreamlu + mica-xss + + + + diff --git a/plusone-basic/plusone-basic-application/src/main/java/xyz/zhouxy/plusone/exception/config/PlusoneExceptionHandlerConfig.java b/plusone-basic/plusone-basic-application/src/main/java/xyz/zhouxy/plusone/exception/config/PlusoneExceptionHandlerConfig.java new file mode 100644 index 0000000..7a06b84 --- /dev/null +++ b/plusone-basic/plusone-basic-application/src/main/java/xyz/zhouxy/plusone/exception/config/PlusoneExceptionHandlerConfig.java @@ -0,0 +1,17 @@ +package xyz.zhouxy.plusone.exception.config; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import xyz.zhouxy.plusone.exception.handler.BaseExceptionHandler.ExceptionInfoHolder; + +@Configuration +public class PlusoneExceptionHandlerConfig { + + @Bean + @ConditionalOnMissingBean + ExceptionInfoHolder exceptionInfoHolder() { + return new ExceptionInfoHolder(); + } +} diff --git a/plusone-basic/plusone-basic-application/src/main/java/xyz/zhouxy/plusone/exception/handler/AllExceptionHandler.java b/plusone-basic/plusone-basic-application/src/main/java/xyz/zhouxy/plusone/exception/handler/AllExceptionHandler.java new file mode 100644 index 0000000..00fb1e4 --- /dev/null +++ b/plusone-basic/plusone-basic-application/src/main/java/xyz/zhouxy/plusone/exception/handler/AllExceptionHandler.java @@ -0,0 +1,29 @@ +package xyz.zhouxy.plusone.exception.handler; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import lombok.extern.slf4j.Slf4j; +import xyz.zhouxy.plusone.util.RestfulResult; + +/** + * 处理所有异常的处理器 + * + * @author ZhouXY + */ +@ConditionalOnProperty(prefix = "plusone.exception", name = "handle-all-exception", havingValue = "true") +@RestControllerAdvice +@Slf4j +public class AllExceptionHandler extends BaseExceptionHandler { + protected AllExceptionHandler(ExceptionInfoHolder exceptionInfoHolder) { + super(exceptionInfoHolder); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Throwable e) { + log.error(e.getMessage(), e); + return this.buildExceptionResponse(e); + } +} diff --git a/plusone-basic/plusone-basic-application/src/main/java/xyz/zhouxy/plusone/exception/handler/DefaultExceptionHandler.java b/plusone-basic/plusone-basic-application/src/main/java/xyz/zhouxy/plusone/exception/handler/DefaultExceptionHandler.java new file mode 100644 index 0000000..e991bb8 --- /dev/null +++ b/plusone-basic/plusone-basic-application/src/main/java/xyz/zhouxy/plusone/exception/handler/DefaultExceptionHandler.java @@ -0,0 +1,63 @@ +package xyz.zhouxy.plusone.exception.handler; + +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.dao.DataAccessException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import lombok.extern.slf4j.Slf4j; +import xyz.zhouxy.plusone.util.RestfulResult; + +/** + * 默认异常的处理器 + * + *

+ * 对 {@link IllegalArgumentException} 异常做了写默认处理。 + *

+ * + *

+ * 将异常信息返回给前端时,隐藏掉数据库异常信息中关于数据库结构、SQL 语句的描述。 + *

+ * + *

+ * 为了避免因为 {@link AllExceptionHandler} 而失效, + * {@code Order} 设置为了 {@value Ordered.LOWEST_PRECEDENCE - 1}。 + * 一般 Spring Bean 的 Order 应该是 Ordered.LOWEST_PRECEDENCE, + * 所以如果想让自定义的异常处理器与本处理器有冲突,希望覆盖本处理器的行为, + * 需要添加 {@link Order} 注解,将 order 修改为比 {@code Ordered.LOWEST_PRECEDENCE - 1} + * 更小的值, + * 如 0 或者 1,甚至是负数。 + *

+ * + * @author ZhouXY + */ +@RestControllerAdvice +@Order(Ordered.LOWEST_PRECEDENCE - 1) +@Slf4j +public class DefaultExceptionHandler extends BaseExceptionHandler { + + public DefaultExceptionHandler(ExceptionInfoHolder exceptionInfoHolder) { + super(exceptionInfoHolder); + set(IllegalArgumentException.class, 4010000, "格式错误", HttpStatus.FORBIDDEN); + set(DataAccessException.class, 6030000, "数据库错误", HttpStatus.INTERNAL_SERVER_ERROR, true); + set(MethodArgumentNotValidException.class, + 4040401, + e -> ((MethodArgumentNotValidException) e).getAllErrors() + .stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .toList() + .toString(), + HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception e) { + log.error(e.getMessage(), e); + return buildExceptionResponse(e); + } +} diff --git a/plusone-basic/plusone-basic-common/pom.xml b/plusone-basic/plusone-basic-common/pom.xml new file mode 100644 index 0000000..f48323f --- /dev/null +++ b/plusone-basic/plusone-basic-common/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + plusone-basic + xyz.zhouxy + 1.0.0-SNAPSHOT + ../pom.xml + + + plusone-basic-common + + + + cn.dev33 + sa-token-core + + + + org.projectlombok + lombok + + + + org.springframework + spring-tx + + + + org.springframework + spring-webmvc + + + + xyz.zhouxy.plusone + plusone-commons + 0.0.1-SNAPSHOT + + + xyz.zhouxy.plusone + plusone-exception-handler + 0.0.1-SNAPSHOT + + + + diff --git a/plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/exception/DataNotExistException.java b/plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/exception/DataNotExistException.java new file mode 100644 index 0000000..c727bc7 --- /dev/null +++ b/plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/exception/DataNotExistException.java @@ -0,0 +1,31 @@ +package xyz.zhouxy.plusone.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * 需要时,当查询数据不存在时抛出的异常 + * + *

+ * 暂时先这样,后续完善异常体系时可能会更改。 + *

+ * + * @author ZhouXY + * @see xyz.zhouxy.plusone.util.AssertResult + */ +@ResponseStatus(HttpStatus.NOT_FOUND) +public class DataNotExistException extends PlusoneException { + + @java.io.Serial + private static final long serialVersionUID = 6536955800679703111L; + + public static final int ERROR_CODE = 4110100; + + public DataNotExistException() { + super(ERROR_CODE, "数据不存在"); + } + + public DataNotExistException(String message) { + super(ERROR_CODE, message); + } +} diff --git a/plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/exception/DataOperationNumberException.java b/plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/exception/DataOperationNumberException.java new file mode 100644 index 0000000..b121bdc --- /dev/null +++ b/plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/exception/DataOperationNumberException.java @@ -0,0 +1,31 @@ +package xyz.zhouxy.plusone.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * 需要时,当数据操作的行数不符合预期时抛出的异常 + * + *

+ * 暂时先这样,后续完善异常体系时可能会更改。 + *

+ * + * @author ZhouXY + * @see xyz.zhouxy.plusone.util.AssertResult + */ +@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) +public class DataOperationNumberException extends PlusoneException { + + @java.io.Serial + private static final long serialVersionUID = -9220765735990318186L; + + public static final int ERROR_CODE = 4110200; + + public DataOperationNumberException() { + super(ERROR_CODE, "数据操作的行数不符合预期"); + } + + public DataOperationNumberException(String message) { + super(ERROR_CODE, message); + } +} diff --git a/plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/exception/InvalidInputException.java b/plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/exception/InvalidInputException.java new file mode 100644 index 0000000..f2a3be9 --- /dev/null +++ b/plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/exception/InvalidInputException.java @@ -0,0 +1,56 @@ +package xyz.zhouxy.plusone.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * 4040200 - 无效的用户输入 + * + * @author ZhouXY + */ +@ResponseStatus(HttpStatus.BAD_REQUEST) +public class InvalidInputException extends PlusoneException { + + @java.io.Serial + private static final long serialVersionUID = 7956661913360059670L; + + public static final int ERROR_CODE = 4040200; + + private InvalidInputException(int code, String msg) { + super(code, msg); + } + + private InvalidInputException(int code, Throwable cause) { + super(code, cause); + } + + private InvalidInputException(int code, String msg, Throwable cause) { + super(code, msg, cause); + } + + public InvalidInputException(String msg) { + this(ERROR_CODE, msg); + } + + public InvalidInputException(Throwable cause) { + this(ERROR_CODE, cause); + } + + public InvalidInputException(String msg, Throwable cause) { + this(ERROR_CODE, msg, cause); + } + + /** + * 不支持的 Principal 类型出现时抛出的异常 + */ + public static InvalidInputException unsupportedPrincipalTypeException() { + return unsupportedPrincipalTypeException("不支持的 PrincipalType"); + } + + /** + * 不支持的 Principal 类型出现时抛出的异常 + */ + public static InvalidInputException unsupportedPrincipalTypeException(String message) { + return new InvalidInputException(4040201, message); + } +} diff --git a/plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/exception/UserOperationException.java b/plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/exception/UserOperationException.java new file mode 100644 index 0000000..77474b8 --- /dev/null +++ b/plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/exception/UserOperationException.java @@ -0,0 +1,58 @@ +package xyz.zhouxy.plusone.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * 4040600 - 用户操作异常 + * + * @author ZhouXY + */ +@ResponseStatus(HttpStatus.BAD_REQUEST) +public class UserOperationException extends PlusoneException { + + @java.io.Serial + private static final long serialVersionUID = 4371055414421991940L; + + public static final int DEFAULT_ERROR_CODE = 4040600; + + public UserOperationException(String msg) { + super(DEFAULT_ERROR_CODE, msg); + } + + public UserOperationException(String msg, Throwable cause) { + super(DEFAULT_ERROR_CODE, msg, cause); + } + + public UserOperationException(int code, String msg) { + super(code, msg); + } + + public UserOperationException(int code, Throwable cause) { + super(code, cause); + } + + public UserOperationException(int code, String msg, Throwable cause) { + super(code, msg, cause); + } + + /** + * 无效的操作 + * + * @return 异常对象 + */ + public static UserOperationException invalidOperation() { + return invalidOperation("无效的操作"); + } + + /** + * 无效的操作 + * + * @param msg 异常信息 + * @return 异常对象 + */ + public static UserOperationException invalidOperation(String msg) { + return new UserOperationException(4040604, msg); + } + +} diff --git a/plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/util/AssertResult.java b/plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/util/AssertResult.java new file mode 100644 index 0000000..0e66185 --- /dev/null +++ b/plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/util/AssertResult.java @@ -0,0 +1,67 @@ +package xyz.zhouxy.plusone.util; + + +import xyz.zhouxy.plusone.exception.DataNotExistException; +import xyz.zhouxy.plusone.exception.DataOperationNumberException; + +import java.util.Objects; + +/** + * 对数据库执行结果进行判断 + * + * @author ZhouXY + */ +public final class AssertResult { + + private AssertResult() { + throw new IllegalStateException("Utility class"); + } + + public static void update(boolean expression) { + if (!expression) { + throw new DataOperationNumberException(); + } + } + + public static void update(boolean expression, String message) { + if (!expression) { + throw new DataOperationNumberException(message); + } + } + + public static void update(Object i, int expectedValue) { + if (!Objects.equals(i, expectedValue)) { + throw new DataOperationNumberException(); + } + } + + public static void update(Object i, int expectedValue, String format) { + if (!Objects.equals(i, expectedValue)) { + throw new DataOperationNumberException(String.format(format, i)); + } + } + + public static void exist(boolean expression) { + if (!expression) { + throw new DataNotExistException(); + } + } + + public static void exist(boolean expression, String message) { + if (!expression) { + throw new DataNotExistException(message); + } + } + + public static void nonNull(Object obj) { + if (Objects.isNull(obj)) { + throw new DataNotExistException(); + } + } + + public static void nonNull(Object obj, String message) { + if (Objects.isNull(obj)) { + throw new DataNotExistException(message); + } + } +} diff --git a/plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/util/PlusoneStrUtil.java b/plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/util/PlusoneStrUtil.java new file mode 100644 index 0000000..f6ce284 --- /dev/null +++ b/plusone-basic/plusone-basic-common/src/main/java/xyz/zhouxy/plusone/util/PlusoneStrUtil.java @@ -0,0 +1,43 @@ +package xyz.zhouxy.plusone.util; + +import java.util.Objects; +import java.util.regex.Pattern; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * 字符串工具类 + * + * @author ZhouXY + */ +public class PlusoneStrUtil { + + private PlusoneStrUtil() { + throw new IllegalStateException("Utility class"); + } + + public static final String EMPTY_STR = ""; + + public static String getTextOrEmpty(String value) { + return StringUtils.hasText(value) ? value : EMPTY_STR; + } + + public static String getStrOrEmpty(String value) { + return StringUtils.hasLength(value) ? value : EMPTY_STR; + } + + public static String checkString(String value, String regex, String message) { + Assert.notNull(value, message); + Assert.isTrue(Pattern.matches(regex, value), message); + return value; + } + + public static String checkStringNullable(String value, String regex, String message) { + return Objects.nonNull(value) ? checkString(value, regex, message) : null; + } + + public static String checkStringOrDefault(String value, String regex, String message) { + return StringUtils.hasText(value) ? checkString(value, regex, message) : ""; + } +} diff --git a/plusone-basic/plusone-basic-domain/pom.xml b/plusone-basic/plusone-basic-domain/pom.xml new file mode 100644 index 0000000..ca01461 --- /dev/null +++ b/plusone-basic/plusone-basic-domain/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + xyz.zhouxy + plusone-basic + 1.0.0-SNAPSHOT + ../pom.xml + + plusone-basic-domain + + + + plusone-basic-common + xyz.zhouxy + + + + cn.hutool + hutool-core + + + + org.projectlombok + lombok + + + + com.fasterxml.jackson.core + jackson-core + 2.13.4 + + + com.fasterxml.jackson.core + jackson-annotations + 2.13.4 + + + diff --git a/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/constant/EntityStatus.java b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/constant/EntityStatus.java new file mode 100644 index 0000000..cc0f7c7 --- /dev/null +++ b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/constant/EntityStatus.java @@ -0,0 +1,32 @@ +package xyz.zhouxy.plusone.constant; + +import xyz.zhouxy.plusone.util.Enumeration; +import xyz.zhouxy.plusone.util.EnumerationValuesHolder; + +/** + * 实体状态 + * + * @author ZhouXY + */ +public class EntityStatus extends Enumeration { + + private EntityStatus(int value, String name) { + super(value, name); + } + + // 常量 + public static final EntityStatus AVAILABLE = new EntityStatus(0, "正常"); + public static final EntityStatus DISABLED = new EntityStatus(1, "禁用"); + + private static final EnumerationValuesHolder ENUMERATION_VALUES = new EnumerationValuesHolder<>( + new EntityStatus[] { AVAILABLE, DISABLED }); + + public static EntityStatus of(int value) { + return ENUMERATION_VALUES.get(value); + } + + @Override + public String toString() { + return "EntityStatus" + super.toString(); + } +} diff --git a/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/constant/RegexConsts.java b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/constant/RegexConsts.java new file mode 100644 index 0000000..a34c436 --- /dev/null +++ b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/constant/RegexConsts.java @@ -0,0 +1,27 @@ +package xyz.zhouxy.plusone.constant; + +/** + * 正则表达式常量 + * + * @author ZhouXY + */ +public final class RegexConsts { + + public static final String DATE = "^\\d{4}-\\d{2}-\\d{2}"; + + public static final String PASSWORD = "^(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])[\\w\\\\!#$%&'*\\+\\-/=?^`{|}~@\\(\\)\\[\\]\",\\.;':><]{8,32}$"; + + public static final String CAPTCHA = "^[0-9A-Za-z]{4,6}$"; + + public static final String EMAIL = "^\\w+([-+.]\\w+)*@[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})*(\\.(?![0-9]+$)[a-zA-Z0-9][-0-9A-Za-z]{0,62})$"; + + public static final String MOBILE_PHONE = "^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$"; + + public static final String USERNAME = "^[\\da-zA-Z_.@\\\\]{4,36}$"; + + public static final String NICKNAME = "^[\\da-zA-Z_.@\\\\]{4,36}$"; + + private RegexConsts() { + throw new IllegalStateException("Utility class"); + } +} diff --git a/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/AggregateRoot.java b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/AggregateRoot.java new file mode 100644 index 0000000..0fb3a66 --- /dev/null +++ b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/AggregateRoot.java @@ -0,0 +1,17 @@ +package xyz.zhouxy.plusone.domain; + +import java.io.Serializable; + +/** + * 聚合根 + * + *

+ * 由 Repository 负责整个聚合根的查询、保存、删除。 + *

+ * + * @author ZhouXY + * @see Entity + * @see IRepository + */ +public abstract class AggregateRoot extends Entity { +} diff --git a/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/DomainEvent.java b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/DomainEvent.java new file mode 100644 index 0000000..34acdda --- /dev/null +++ b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/DomainEvent.java @@ -0,0 +1,26 @@ +package xyz.zhouxy.plusone.domain; + +import cn.hutool.core.lang.UUID; +import lombok.Getter; + +/** + * 领域事件 + * + *

+ * 根据所使用的消息机制的不同,可能需要继承其它类,或实现 {@link java.io.Serializable} 接口 + *

+ * + * @author ZhouXY + * @see Entity + * @see IEventHandler + */ +@Getter +public abstract class DomainEvent { + private String identifier; + private long happenedAt; + + protected DomainEvent() { + this.identifier = UUID.randomUUID().toString(true); + this.happenedAt = System.currentTimeMillis(); + } +} diff --git a/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/Entity.java b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/Entity.java new file mode 100644 index 0000000..06daaf6 --- /dev/null +++ b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/Entity.java @@ -0,0 +1,32 @@ +package xyz.zhouxy.plusone.domain; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * 实体 + * + *

+ * DDD 中的实体,带有 ID。 + *

+ * + *

+ * 维护一个 {@link DomainEvent} 的列表,持久化时可将其中的领域事件进行发布。 + *

+ * + * @author ZhouXY + * @see IValueObject + * @see AggregateRoot + */ +public abstract class Entity { + + private final List domainEvents = new ArrayList<>(); + + public abstract Optional getId(); + + protected void addDomainEvent(DomainEvent domainEvent) { + domainEvents.add(domainEvent); + } +} diff --git a/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/ICommand.java b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/ICommand.java new file mode 100644 index 0000000..de275f5 --- /dev/null +++ b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/ICommand.java @@ -0,0 +1,10 @@ +package xyz.zhouxy.plusone.domain; + +/** + * 命令 + * + * @author ZhouXY + * @see ICommandHandler + */ +public interface ICommand { +} diff --git a/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/ICommandHandler.java b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/ICommandHandler.java new file mode 100644 index 0000000..a3feaf8 --- /dev/null +++ b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/ICommandHandler.java @@ -0,0 +1,12 @@ +package xyz.zhouxy.plusone.domain; + +/** + * 命令处理器 + * + * @author ZhouXY + * @see ICommand + */ +@FunctionalInterface +public interface ICommandHandler { + void handle(T command); +} diff --git a/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IEventHandler.java b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IEventHandler.java new file mode 100644 index 0000000..d49299a --- /dev/null +++ b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IEventHandler.java @@ -0,0 +1,12 @@ +package xyz.zhouxy.plusone.domain; + +/** + * 事件处理器 + * + * @author ZhouXY + * @see DomainEvent + */ +@FunctionalInterface +public interface IEventHandler { + void handle(E command); +} diff --git a/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IRepository.java b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IRepository.java new file mode 100644 index 0000000..f913a4b --- /dev/null +++ b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IRepository.java @@ -0,0 +1,21 @@ +package xyz.zhouxy.plusone.domain; + +import java.io.Serializable; + +/** + * Repository 基础接口 + * + * @author ZhouXY + * @see AggregateRoot + */ +public interface IRepository, ID extends Serializable> { + + T find(ID id); + + T save(T entity); + + void delete(T entity); + + boolean exists(ID id); + +} diff --git a/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IValueObject.java b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IValueObject.java new file mode 100644 index 0000000..4c6b0a6 --- /dev/null +++ b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IValueObject.java @@ -0,0 +1,9 @@ +package xyz.zhouxy.plusone.domain; + +/** + * 值对象 + * + * @author ZhouXY + */ +public interface IValueObject { +} diff --git a/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IWithLabel.java b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IWithLabel.java new file mode 100644 index 0000000..156c6b9 --- /dev/null +++ b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IWithLabel.java @@ -0,0 +1,10 @@ +package xyz.zhouxy.plusone.domain; + +/** + * 带可读的 label 属性 + * + * @author ZhouXY + */ +public interface IWithLabel { + String getLabel(); +} diff --git a/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IWithOrderNumber.java b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IWithOrderNumber.java new file mode 100644 index 0000000..7c7e151 --- /dev/null +++ b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IWithOrderNumber.java @@ -0,0 +1,33 @@ +package xyz.zhouxy.plusone.domain; + +import java.util.Comparator; +import java.util.Objects; + +/** + * 带 orderNumber 字段,可用来排序 + * + * @author ZhouXY + */ +public interface IWithOrderNumber extends Comparable { + + int getOrderNumber(); + + @Override + default int compareTo(IWithOrderNumber that) { + return new OrderNumberComparator().compare(this, that); + } + + class OrderNumberComparator> + implements Comparator { + @Override + public int compare(IWithOrderNumber a, IWithOrderNumber b) { + if (Objects.equals(a, b)) { + return 0; + } + if (a == null) { + return -1; + } + return (b == null) ? 1 : Integer.compare(a.getOrderNumber(), b.getOrderNumber()); + } + } +} diff --git a/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IWithVersion.java b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IWithVersion.java new file mode 100644 index 0000000..068b2bf --- /dev/null +++ b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/IWithVersion.java @@ -0,0 +1,10 @@ +package xyz.zhouxy.plusone.domain; + +/** + * 带版本号 + * + * @author ZhouXY + */ +public interface IWithVersion { + long getVersion(); +} diff --git a/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/ValidatableStringRecord.java b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/ValidatableStringRecord.java new file mode 100644 index 0000000..7a0e8ec --- /dev/null +++ b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/domain/ValidatableStringRecord.java @@ -0,0 +1,34 @@ +package xyz.zhouxy.plusone.domain; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 带校验的字符串值对象 + * + * @author ZhouXY + */ +public abstract class ValidatableStringRecord implements IValueObject { + + protected String value; + protected final String format; + + protected ValidatableStringRecord(String format) { + this.format = format; + } + + @JsonIgnore + protected boolean isValid() { + return value.matches(format); + } + + @JsonValue + public String value() { + return value; + } + + @Override + public String toString() { + return value; + } +} diff --git a/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/util/RegexUtil.java b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/util/RegexUtil.java new file mode 100644 index 0000000..d0a5538 --- /dev/null +++ b/plusone-basic/plusone-basic-domain/src/main/java/xyz/zhouxy/plusone/util/RegexUtil.java @@ -0,0 +1,36 @@ +package xyz.zhouxy.plusone.util; + +import java.util.regex.Pattern; + +public class RegexUtil { + + private RegexUtil() { + throw new IllegalStateException("Utility class"); + } + + public static boolean matches(CharSequence input, String regex) { + return Pattern.matches(regex, input); + } + + public static boolean matchesOr(CharSequence input, String... regexs) { + boolean isMatched; + for (var regex : regexs) { + isMatched = Pattern.matches(regex, input); + if (isMatched) { + return true; + } + } + return false; + } + + public static boolean matchesAnd(CharSequence input, String... regexs) { + boolean isMatched; + for (var regex : regexs) { + isMatched = Pattern.matches(regex, input); + if (!isMatched) { + return false; + } + } + return true; + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/pom.xml b/plusone-basic/plusone-basic-infrastructure/pom.xml new file mode 100644 index 0000000..0a32200 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/pom.xml @@ -0,0 +1,95 @@ + + + 4.0.0 + + xyz.zhouxy + plusone-basic + 1.0.0-SNAPSHOT + ../pom.xml + + plusone-basic-infrastructure + + + + + xyz.zhouxy + plusone-basic-common + + + xyz.zhouxy + plusone-basic-domain + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-aop + + + com.baomidou + mybatis-plus-boot-starter + + + org.springframework.boot + spring-boot-starter-data-jdbc + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-mail + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-devtools + runtime + + + + org.postgresql + postgresql + runtime + + + org.projectlombok + lombok + + + + cn.dev33 + sa-token-spring-boot-starter + + + + cn.dev33 + sa-token-dao-redis-jackson + + + + org.springframework.boot + spring-boot-autoconfigure + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + com.tencentcloudapi + tencentcloud-sdk-java-sms + + + + diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/config/PlusoneConfig.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/config/PlusoneConfig.java new file mode 100644 index 0000000..7772cbb --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/config/PlusoneConfig.java @@ -0,0 +1,14 @@ +package xyz.zhouxy.plusone.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +/** + * Plusone 配置。加载 plusone.properties 文件 + * + * @author ZhouXY + */ +@Configuration +@PropertySource(value = {"classpath:conf/plusone.properties"}, encoding = "utf-8") +public class PlusoneConfig { +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/infrastructure/pojo/AbstractEntityPO.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/infrastructure/pojo/AbstractEntityPO.java new file mode 100644 index 0000000..de65a41 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/infrastructure/pojo/AbstractEntityPO.java @@ -0,0 +1,41 @@ +package xyz.zhouxy.plusone.infrastructure.pojo; + +import java.time.LocalDateTime; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * 实体的持久化对象 + * + *

+ * 由于本项目目前是手动实现 Repository,直接将实体持久化, + * 查询结果也是直接实例化为 Entity,故暂时不使用 PO。 + *

+ * + * @author ZhouXY + */ +@Getter +@Setter +@ToString +public abstract class AbstractEntityPO { + protected Long createdBy; + protected LocalDateTime createTime; + protected Long updatedBy; + protected LocalDateTime updateTime; + + public abstract Long getId(); + + public void auditFields(Long updatedBy, LocalDateTime updateTime) { + this.updatedBy = updatedBy; + this.updateTime = updateTime; + } + + public void auditFields(Long createdBy, LocalDateTime createTime, Long updatedBy, LocalDateTime updateTime) { + this.createdBy = createdBy; + this.createTime = createTime; + this.updatedBy = updatedBy; + this.updateTime = updateTime; + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/BeanPropertyParamSource.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/BeanPropertyParamSource.java new file mode 100644 index 0000000..bcb72b0 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/BeanPropertyParamSource.java @@ -0,0 +1,33 @@ +package xyz.zhouxy.plusone.jdbc; + +import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; +import org.springframework.lang.Nullable; + +/** + * 扩展了 {@link BeanPropertySqlParameterSource},在将 POJO 转换为 + * {@link org.springframework.jdbc.core.namedparam.SqlParameterSource} 时, + * 使用 {@link Enum#ordinal()} 将枚举转换成整数。 + * + * @author ZhouXY + * + * @see SqlParameterSource + * @see BeanPropertySqlParameterSource + * @see org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate + * @see Enum + */ +public class BeanPropertyParamSource extends BeanPropertySqlParameterSource { + + public BeanPropertyParamSource(Object object) { + super(object); + } + + @Override + @Nullable + public Object getValue(String paramName) throws IllegalArgumentException { + Object value = super.getValue(paramName); + if (value instanceof Enum) { + return ((Enum) value).ordinal(); + } + return value; + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/JdbcConfig.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/JdbcConfig.java new file mode 100644 index 0000000..01041e2 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/JdbcConfig.java @@ -0,0 +1,51 @@ +package xyz.zhouxy.plusone.jdbc; + +import java.util.List; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; +import org.springframework.data.jdbc.repository.QueryMappingConfiguration; +import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration; +import org.springframework.data.jdbc.repository.config.DefaultQueryMappingConfiguration; + +import xyz.zhouxy.plusone.jdbc.converter.EnumToOrdinalConverter; + +/** + * JDBC 配置 + * + *

+ * 配置了类型转换器 + *

+ * + *

+ * 这里声明了 {@link DefaultQueryMappingConfiguration} 的实例为 Spring bean。 + * 在其它配置类中注入该 bean,就可以向其中添加 + * {@link org.springframework.jdbc.core.RowMapper} + * 供 Spring Data JDBC 查询时使用。如下所示: + * + *

+ * {@code @Configuration}
+ * class CustomConfig {
+ *     public CustomConfig(DefaultQueryMappingConfiguration rowMappers) {
+ *         rowMappers.registerRowMapper(Person.class, new PersonRowMapper());
+ *     }
+ * }
+ * 
+ *

+ * + * @author ZhouXY + */ +@Configuration +public class JdbcConfig extends AbstractJdbcConfiguration { + + @Override + public JdbcCustomConversions jdbcCustomConversions() { + return new JdbcCustomConversions(List.of(EnumToOrdinalConverter.INSTANCE)); + } + + @Bean + QueryMappingConfiguration rowMappers() { + return new DefaultQueryMappingConfiguration(); + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/JdbcEntityDaoSupport.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/JdbcEntityDaoSupport.java new file mode 100644 index 0000000..96ec4a4 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/JdbcEntityDaoSupport.java @@ -0,0 +1,68 @@ +package xyz.zhouxy.plusone.jdbc; + +import java.io.Serializable; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.stream.Stream; + +import javax.annotation.Nonnull; + +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; + +import xyz.zhouxy.plusone.domain.Entity; + +public abstract class JdbcEntityDaoSupport, ID extends Serializable> { + protected final NamedParameterJdbcTemplate jdbc; + + protected RowMapper rowMapper; + protected ResultSetExtractor resultSetExtractor; + + protected JdbcEntityDaoSupport(@Nonnull NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + this.jdbc = namedParameterJdbcTemplate; + this.rowMapper = (ResultSet rs, int rowNum) -> mapRow(rs); + this.resultSetExtractor = (ResultSet rs) -> rs.next() ? mapRow(rs) : null; + } + + protected final T queryForObject(String sql) { + return this.jdbc.query(sql, this.resultSetExtractor); + } + + protected final T queryForObject(String sql, SqlParameterSource paramSource) { + return this.jdbc.query(sql, paramSource, this.resultSetExtractor); + } + + protected final List queryForList(String sql) { + return this.jdbc.query(sql, this.rowMapper); + } + + protected final List queryForList(String sql, SqlParameterSource parameterSource) { + return this.jdbc.query(sql, parameterSource, this.rowMapper); + } + + protected final Stream queryForStream(String sql, SqlParameterSource parameterSource) { + return this.jdbc.queryForStream(sql, parameterSource, this.rowMapper); + } + + protected final Stream queryForStream(String sql, SqlParameterSource parameterSource, Class elementType) { + return this.jdbc.queryForList(sql, parameterSource, elementType).stream(); + } + + protected final boolean queryExists(String sql, SqlParameterSource parameterSource) { + Boolean isExists = this.jdbc.query(sql, parameterSource, ResultSet::next); + return Boolean.TRUE.equals(isExists); + } + + protected abstract T mapRow(ResultSet rs) throws SQLException; + + protected void setRowMapper(@Nonnull RowMapper rowMapper) { + this.rowMapper = rowMapper; + } + + protected void setResultSetExtractor(@Nonnull ResultSetExtractor resultSetExtractor) { + this.resultSetExtractor = resultSetExtractor; + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/JdbcFactory.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/JdbcFactory.java new file mode 100644 index 0000000..d3acc45 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/JdbcFactory.java @@ -0,0 +1,30 @@ +package xyz.zhouxy.plusone.jdbc; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; + +import xyz.zhouxy.plusone.spring.SpringContextHolder; + +/** + * 全局单例,由 Spring 装配好的对象。 + * 可通过静态方法获取 Spring 容器中的 {@link JdbcTemplate} 和 + * {@link NamedParameterJdbcTemplate} 对象。 + * + * @author ZhouXY + * @see JdbcTemplate + * @see NamedParameterJdbcTemplate + */ +public final class JdbcFactory { + + private JdbcFactory() { + throw new IllegalStateException("Utility class"); + } + + public static JdbcTemplate getJdbcTemplate() { + return SpringContextHolder.getContext().getBean(JdbcTemplate.class); + } + + public static NamedParameterJdbcTemplate getNamedParameterJdbcTemplate() { + return SpringContextHolder.getContext().getBean(NamedParameterJdbcTemplate.class); + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/JdbcRepositorySupport.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/JdbcRepositorySupport.java new file mode 100644 index 0000000..3b44b60 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/JdbcRepositorySupport.java @@ -0,0 +1,58 @@ +package xyz.zhouxy.plusone.jdbc; + +import java.io.Serializable; + +import javax.annotation.Nonnull; + +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; + +import xyz.zhouxy.plusone.domain.AggregateRoot; +import xyz.zhouxy.plusone.domain.IRepository; + +public abstract class JdbcRepositorySupport, ID extends Serializable> + extends JdbcEntityDaoSupport + implements IRepository { + + protected JdbcRepositorySupport(@Nonnull NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + super(namedParameterJdbcTemplate); + } + + protected abstract void doDelete(@Nonnull T entity); + + protected abstract T doFindById(@Nonnull ID id); + + protected abstract T doInsert(@Nonnull T entity); + + protected abstract T doUpdate(@Nonnull T entity); + + @Override + public final void delete(T entity) { + if (entity == null) { + throw new IllegalArgumentException("Cannot delete null."); + } + doDelete(entity); + } + + @Override + public final T find(ID id) { + if (id == null) { + throw new IllegalArgumentException("Id cannot be null."); + } + return doFindById(id); + } + + @Override + public final T save(T entity) { + if (entity == null) { + throw new IllegalArgumentException("Cannot save null."); + } + return entity.getId().isPresent() ? doUpdate(entity) : doInsert(entity); + } + + protected abstract SqlParameterSource generateParamSource(ID id, @Nonnull T entity); + + protected final SqlParameterSource generateParamSource(@Nonnull T entity) { + return generateParamSource(entity.getId().orElseThrow(), entity); + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/common/ResultMapper.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/common/ResultMapper.java new file mode 100644 index 0000000..55cf363 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/common/ResultMapper.java @@ -0,0 +1,45 @@ +package xyz.zhouxy.plusone.jdbc.common; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * 查询结果处理 + * + *

+ * 通过在 {@link #map(ResultSet)} 中配置 {@link ResultSet} 到对象的映射, + * 可将 {@link #rowMapper(ResultSet, int)} 的方法应用, + * 直接当成 {@link org.springframework.jdbc.core.RowMapper} 对象传给 + * {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate} + * 的查询方法, + * 或者在 Spring Data JDBC 的配置中使用。 + *

+ * + *

+ * 在 + * {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate} + * 的 query(String, java.util.Map, ResultSetExtractor) + * 和 query(String, SqlParameterSource, ResultSetExtractor) + * 两个方法执行时,ResultSetExtractor 中需要执行一次 {@link ResultSet#next()} 判断是否查询到数据, + * 如果查询到数据再执行查询结果的实例化。 + * {@link #resultSetExtractor(ResultSet)} 封装了这个过程, + * 和 {@link #rowMapper(ResultSet, int)}一样, + * 直接把方法引用当成 {@link org.springframework.jdbc.core.ResultSetExtractor} 的对象即可。 + *

+ * + * @author ZhouXY + * @see org.springframework.data.jdbc.repository.config.DefaultQueryMappingConfiguration + */ +@FunctionalInterface +public interface ResultMapper { + + T map(ResultSet resultSet) throws SQLException; + + default T rowMapper(ResultSet resultSet, int rowNum) throws SQLException { + return map(resultSet); + } + + default T resultSetExtractor(ResultSet resultSet) throws SQLException { + return resultSet.next() ? map(resultSet) : null; + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/common/SimpleResultMapper.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/common/SimpleResultMapper.java new file mode 100644 index 0000000..0420538 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/common/SimpleResultMapper.java @@ -0,0 +1,30 @@ +package xyz.zhouxy.plusone.jdbc.common; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.RowMapper; + +public class SimpleResultMapper implements ResultMapper { + + private final RowMapper rowMapper; + + public SimpleResultMapper(RowMapper rowMapper) { + this.rowMapper = rowMapper; + } + + @Override + public T map(ResultSet resultSet) throws SQLException { + return rowMapper.mapRow(resultSet, 1); + } + + @Override + public T rowMapper(ResultSet resultSet, int rowNum) throws SQLException { + return this.rowMapper.mapRow(resultSet, rowNum); + } + + public static SimpleResultMapper of(Class clazz) { + return new SimpleResultMapper<>(new BeanPropertyRowMapper<>(clazz)); + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/converter/EnumToOrdinalConverter.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/converter/EnumToOrdinalConverter.java new file mode 100644 index 0000000..85af739 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/converter/EnumToOrdinalConverter.java @@ -0,0 +1,20 @@ +package xyz.zhouxy.plusone.jdbc.converter; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.WritingConverter; + +/** + * 枚举序号转换器 + * + * @author ZhouXY + */ +@WritingConverter +public enum EnumToOrdinalConverter implements Converter, Integer> { + INSTANCE + ; + + @Override + public Integer convert(Enum source) { + return source.ordinal(); + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/converter/OrdinalToEnumConverter.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/converter/OrdinalToEnumConverter.java new file mode 100644 index 0000000..d62f17a --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/jdbc/converter/OrdinalToEnumConverter.java @@ -0,0 +1,44 @@ +package xyz.zhouxy.plusone.jdbc.converter; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; + +/** + * 序号枚举转换器 + *

+ * 向 {@link org.springframework.data.jdbc.core.convert.JdbcCustomConversions} + * 中添加类型转换器, + * 如: + * + *

+ * return new JdbcCustomConversions(List.of(new OrdinalToEnumConverter()));
+ * 
+ *

+ * + * @author ZhouXY + * + * @see Converter + * @see org.springframework.data.jdbc.core.convert.JdbcCustomConversions + */ +@ReadingConverter +public class OrdinalToEnumConverter> implements Converter> { + + private final Class type; + private final E[] constants; + + public OrdinalToEnumConverter(Class type) { + this.type = type; + this.constants = type.getEnumConstants(); + } + + @Override + public Enum convert(Integer ordinal) { + try { + return constants[ordinal]; + } catch (ArrayIndexOutOfBoundsException exception) { + throw new IllegalArgumentException( + String.format("Cannot convert %d to %s by ordinal value.", ordinal, type.getSimpleName()), + exception); + } + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mail/MailMessageFactory.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mail/MailMessageFactory.java new file mode 100644 index 0000000..330d4cc --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mail/MailMessageFactory.java @@ -0,0 +1,31 @@ +package xyz.zhouxy.plusone.mail; + +import org.springframework.mail.SimpleMailMessage; + +/** + * 构建 {@link SimpleMailMessage} + * + * @author ZhouXY + */ +public class MailMessageFactory { + private final PlusoneMailProperties mailProperties; + + public MailMessageFactory(PlusoneMailProperties mailProperties) { + this.mailProperties = mailProperties; + } + + public SimpleMailMessage getCodeMailMessage(String code, String to) { + String subject = mailProperties.getSubject().get("code"); + String content = String.format(mailProperties.getTemplate().get("code"), code); + return getSimpleMailMessage(to, subject, content); + } + + public SimpleMailMessage getSimpleMailMessage(String to, String subject, String content) { + SimpleMailMessage simpleMailMessage = new SimpleMailMessage(); + simpleMailMessage.setFrom(mailProperties.getFrom()); + simpleMailMessage.setTo(to); + simpleMailMessage.setSubject(subject); + simpleMailMessage.setText(content); + return simpleMailMessage; + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mail/MailService.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mail/MailService.java new file mode 100644 index 0000000..3590015 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mail/MailService.java @@ -0,0 +1,17 @@ +package xyz.zhouxy.plusone.mail; + +import javax.mail.MessagingException; + +/** + * 邮件服务 + * + * @author ZhouXY + */ +public interface MailService { + + void sendSimpleMail(String to, String subject, String content); + + void sendHtmlMail(String to, String subject, String content) throws MessagingException; + + void sendCodeMail(String code, String to); +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mail/PlusoneMailAutoConfiguration.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mail/PlusoneMailAutoConfiguration.java new file mode 100644 index 0000000..69db2b2 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mail/PlusoneMailAutoConfiguration.java @@ -0,0 +1,26 @@ +package xyz.zhouxy.plusone.mail; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; + +/** + * 读取配置文件,实例化 {@link MailService} 对象后,放到 Spring 容器中。 + * + * @author ZhouXY + */ +@Configuration +@EnableConfigurationProperties(PlusoneMailProperties.class) +@ConditionalOnClass(MailService.class) +@EnableAutoConfiguration +public class PlusoneMailAutoConfiguration { + + @Bean + public MailService mailService(JavaMailSender mailSender, PlusoneMailProperties mailProperties) { + MailMessageFactory mailMessageFactory = new MailMessageFactory(mailProperties); + return new SimpleMailService(mailSender, mailMessageFactory); + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mail/PlusoneMailProperties.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mail/PlusoneMailProperties.java new file mode 100644 index 0000000..74eb2c8 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mail/PlusoneMailProperties.java @@ -0,0 +1,16 @@ +package xyz.zhouxy.plusone.mail; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.Map; + +@Getter +@Setter +@ConfigurationProperties("plusone.mail") +public class PlusoneMailProperties { + private String from; + private Map subject; + private Map template; +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mail/SimpleMailService.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mail/SimpleMailService.java new file mode 100644 index 0000000..e333994 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mail/SimpleMailService.java @@ -0,0 +1,57 @@ +package xyz.zhouxy.plusone.mail; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.mail.MailException; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; + +import javax.mail.MessagingException; + +/** + * 邮件服务。不能发送 HTML 邮件。 + * + * @author ZhouXY + */ +@Slf4j +public class SimpleMailService implements MailService { + + private final JavaMailSender mailSender; + private final MailMessageFactory mailMessageFactory; + + public SimpleMailService(JavaMailSender mailSender, MailMessageFactory mailMessageFactory) { + this.mailSender = mailSender; + this.mailMessageFactory = mailMessageFactory; + } + + /** + * 发送文本邮件 + */ + @Override + public void sendSimpleMail(String to, String subject, String content) { + SimpleMailMessage message = mailMessageFactory.getSimpleMailMessage(to, subject, content); + try { + mailSender.send(message); + log.debug("简单邮件已经发送。"); + } catch (MailException e) { + log.error("发送简单邮件时发生异常!", e); + throw e; + } + } + + @Override + public void sendHtmlMail(String to, String subject, String content) throws MessagingException { + throw new UnsupportedOperationException("暂不支持"); + } + + @Override + public void sendCodeMail(String code, String to) { + SimpleMailMessage message = mailMessageFactory.getCodeMailMessage(code, to); + try { + mailSender.send(message); + log.debug("简单邮件已经发送。"); + } catch (MailException e) { + log.error("发送简单邮件时发生异常!", e); + throw e; + } + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mybatis/MyBatisAutoConfiguration.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mybatis/MyBatisAutoConfiguration.java new file mode 100644 index 0000000..2191a44 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mybatis/MyBatisAutoConfiguration.java @@ -0,0 +1,17 @@ +package xyz.zhouxy.plusone.mybatis; + +import org.apache.ibatis.session.SqlSessionFactory; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableAutoConfiguration +public class MyBatisAutoConfiguration { + + @Bean + MybatisUtil mybatisUtil(SqlSessionFactory sqlSessionFactory) { + return MybatisUtil.getInstance() + .setSqlSessionFactory(sqlSessionFactory); + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mybatis/MyBatisPlusConfig.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mybatis/MyBatisPlusConfig.java new file mode 100644 index 0000000..0ba5c2c --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mybatis/MyBatisPlusConfig.java @@ -0,0 +1,19 @@ +package xyz.zhouxy.plusone.mybatis; + +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MyBatisPlusConfig { + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); + interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); + return interceptor; + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mybatis/MybatisUtil.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mybatis/MybatisUtil.java new file mode 100644 index 0000000..7a33ff5 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/mybatis/MybatisUtil.java @@ -0,0 +1,28 @@ +package xyz.zhouxy.plusone.mybatis; + +import org.apache.ibatis.session.SqlSessionFactory; + +public final class MybatisUtil { + + private SqlSessionFactory sqlSessionFactory; + + private MybatisUtil() { + } + + MybatisUtil setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) { + this.sqlSessionFactory = sqlSessionFactory; + return this; + } + + private static final class Holder { + private static final MybatisUtil INSTANCE = new MybatisUtil(); + } + + public static MybatisUtil getInstance() { + return Holder.INSTANCE; + } + + public static SqlSessionFactory getSqlSessionFactory() { + return MybatisUtil.getInstance().sqlSessionFactory; + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/redis/RedisStrCacheDAO.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/redis/RedisStrCacheDAO.java new file mode 100644 index 0000000..3c4e94a --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/redis/RedisStrCacheDAO.java @@ -0,0 +1,42 @@ +package xyz.zhouxy.plusone.redis; + +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Repository; + +import java.util.concurrent.TimeUnit; + +/** + * 使用 Redis 存放临时字符串 + * + * @author ZhouXY + */ +@Repository +public class RedisStrCacheDAO implements StrCacheDAO { + + private final StringRedisTemplate template; + + public RedisStrCacheDAO(StringRedisTemplate template) { + this.template = template; + } + + @Override + public void setValue(String key, String value, long timeout, TimeUnit unit) { + ValueOperations ops = template.opsForValue(); + ops.set(key, value, timeout, unit); + } + + @Override + public String getValue(String key) { + ValueOperations ops = this.template.opsForValue(); + return ops.get(key); + } + + @Override + public String getValueAndDelete(String key) { + ValueOperations ops = this.template.opsForValue(); + String value = ops.get(key); + template.delete(key); + return value; + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/redis/StrCacheDAO.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/redis/StrCacheDAO.java new file mode 100644 index 0000000..a6ec8e4 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/redis/StrCacheDAO.java @@ -0,0 +1,17 @@ +package xyz.zhouxy.plusone.redis; + +import java.util.concurrent.TimeUnit; + +/** + * 字符串缓存接口 + * + * @author ZhouXY + */ +public interface StrCacheDAO { + + void setValue(String key, String value, long timeout, TimeUnit unit); + + String getValue(String key); + + String getValueAndDelete(String key); +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/sms/PlusoneSmsAutoConfiguration.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/sms/PlusoneSmsAutoConfiguration.java new file mode 100644 index 0000000..f712f41 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/sms/PlusoneSmsAutoConfiguration.java @@ -0,0 +1,27 @@ +package xyz.zhouxy.plusone.sms; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * SMS 相关配置 + * + * @author ZhouXY + */ +@Configuration +@EnableConfigurationProperties(value = { + SmsProperties.class, + SmsCredentialProperties.class, + SmsClientProperties.class, + SmsHttpProperties.class, + SmsProxyProperties.class}) +@ConditionalOnClass(SmsService.class) +public class PlusoneSmsAutoConfiguration { + + @Bean + public SmsService smsService(SmsProperties smsProperties) { + return new TencentSmsServiceImpl(smsProperties); + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/sms/SmsProperties.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/sms/SmsProperties.java new file mode 100644 index 0000000..7107cd8 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/sms/SmsProperties.java @@ -0,0 +1,51 @@ +package xyz.zhouxy.plusone.sms; + +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import lombok.Data; + +/** + * SMS 相关参数 + * + * @author ZhouXY + */ +@Data +@ConfigurationProperties("plusone.sms") +public class SmsProperties { + private String region; + private SmsCredentialProperties credential; + private SmsClientProperties client; + private String appId; + private Map templates; +} + +@Data +@ConfigurationProperties("plusone.sms.credential") +class SmsCredentialProperties { + private String secretId; + private String secretKey; +} + +@Data +@ConfigurationProperties("plusone.sms.client") +class SmsClientProperties { + private String signMethod; + private SmsHttpProperties http; +} + +@Data +@ConfigurationProperties("plusone.sms.client.http") +class SmsHttpProperties { + private SmsProxyProperties proxy; + private String reqMethod; + private Integer connTimeout; +} + +@Data +@ConfigurationProperties("plusone.sms.client.http.proxy") +class SmsProxyProperties { + private String host; + private Integer port; +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/sms/SmsService.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/sms/SmsService.java new file mode 100644 index 0000000..2e872a1 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/sms/SmsService.java @@ -0,0 +1,13 @@ +package xyz.zhouxy.plusone.sms; + +/** + * SMS 服务 + * + * @author ZhouXY + */ +public interface SmsService { + + void sendCodeMessage(String code, String phoneNumber); + + void sendCodeMessage(String signName, String code, String phoneNumber); +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/sms/TencentSmsServiceImpl.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/sms/TencentSmsServiceImpl.java new file mode 100644 index 0000000..2b79f97 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/sms/TencentSmsServiceImpl.java @@ -0,0 +1,145 @@ +package xyz.zhouxy.plusone.sms; + +import com.tencentcloudapi.common.Credential; +import com.tencentcloudapi.common.exception.TencentCloudSDKException; +import com.tencentcloudapi.common.profile.ClientProfile; +import com.tencentcloudapi.common.profile.HttpProfile; +import com.tencentcloudapi.sms.v20210111.SmsClient; +import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest; +import org.springframework.stereotype.Service; + +import lombok.extern.slf4j.Slf4j; + +/** + * 使用腾讯 SMS 服务 + * + * @author ZhouXY + */ +@Service +@Slf4j +public class TencentSmsServiceImpl implements SmsService { + + private final SmsProperties properties; + private final Credential credential; + + public TencentSmsServiceImpl(SmsProperties properties) { + this.properties = properties; + SmsCredentialProperties smsCredential = properties.getCredential(); + this.credential = new Credential(smsCredential.getSecretId(), smsCredential.getSecretKey()); + } + + @Override + public void sendCodeMessage(String code, String phoneNumber) { + sendCodeMessage("ZhouXY", code, phoneNumber); + } + + @Override + public void sendCodeMessage(String signName, String code, String phoneNumber) { + String[] phoneNums = {"+86" + phoneNumber}; + sendMessage(signName, "code", phoneNums, code, "10"); + } + + public void sendMessage(String signName, String action, String[] phoneNumberSet, String... templateParamSet) { + try { + + SmsClient client = getClient(); + SendSmsRequest req = getSendSmsRequest(signName, + properties.getTemplates().get(action), + phoneNumberSet, + templateParamSet); + + /* + * 通过 client 对象调用 SendSms 方法发起请求。注意请求方法名与请求对象是对应的 返回的 res 是一个 + * SendSmsResponse 类的实例,与请求对象对应 + */ + // var res = client.SendSms(req); + client.SendSms(req); + + // 输出json格式的字符串回包 + // System.out.println(SendSmsResponse.toJsonString(res)); + + // 也可以取出单个值,你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义 + // System.out.println(res.getRequestId()); + + } catch (TencentCloudSDKException e) { + log.error(e.getMessage(), e); + } + } + + private SendSmsRequest getSendSmsRequest(String signName, + String templateId, + String[] phoneNumberSet, + String... templateParamSet) { + SendSmsRequest req = new SendSmsRequest(); + + String sdkAppId = properties.getAppId(); + req.setSmsSdkAppId(sdkAppId); + req.setSignName(signName); + + /* 国际/港澳台短信 SenderId: 国内短信填空,默认未开通,如需开通请联系 [sms helper] */ + String senderId = ""; + req.setSenderId(senderId); + + /* 用户的 session 内容: 可以携带用户侧 ID 等上下文信息,server 会原样返回 */ + // String sessionContext = "xxx"; + // req.setSessionContext(sessionContext); + + /* 短信号码扩展号: 默认未开通,如需开通请联系 [sms helper] */ + String extendCode = ""; + req.setExtendCode(extendCode); + + /* 模板 ID: 必须填写已审核通过的模板 ID。模板ID可登录 [短信控制台] 查看 */ + req.setTemplateId(templateId); + + /* + * 下发手机号码,采用 E.164 标准,+[国家或地区码][手机号] 示例如:+8613711112222, 其中前面有一个+号 + * ,86为国家码,13711112222为手机号,最多不要超过200个手机号 + */ + req.setPhoneNumberSet(phoneNumberSet); + + /* 模板参数: 若无模板参数,则设置为空 */ + req.setTemplateParamSet(templateParamSet); + return req; + } + + private SmsClient getClient() { + String region = properties.getRegion(); + ClientProfile clientProfile = getClientProfile(); + return new SmsClient(credential, region, clientProfile); + } + + private ClientProfile getClientProfile() { + HttpProfile httpProfile = getHttpProfile(); + /* + * 非必要步骤: 实例化一个客户端配置对象,可以指定超时时间等配置 + */ + ClientProfile clientProfile = new ClientProfile(); + String signMethod = properties.getClient().getSignMethod(); + if (signMethod != null) { + clientProfile.setSignMethod(signMethod); + } else { + clientProfile.setSignMethod("HmacSHA256"); + } + clientProfile.setHttpProfile(httpProfile); + return clientProfile; + } + + private HttpProfile getHttpProfile() { + HttpProfile httpProfile = new HttpProfile(); + SmsHttpProperties smsHttp = properties.getClient().getHttp(); + if (smsHttp != null) { + if (smsHttp.getReqMethod() != null) { + httpProfile.setReqMethod(smsHttp.getReqMethod()); + } + if (smsHttp.getConnTimeout() != null) { + httpProfile.setConnTimeout(60); + } + SmsProxyProperties proxy = smsHttp.getProxy(); + if (proxy != null && proxy.getHost() != null && proxy.getPort() != null) {// 设置代理 + httpProfile.setProxyHost(proxy.getHost()); + httpProfile.setProxyPort(proxy.getPort()); + } + } + return httpProfile; + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/spring/SpringContextHolder.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/spring/SpringContextHolder.java new file mode 100644 index 0000000..089cee5 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/spring/SpringContextHolder.java @@ -0,0 +1,28 @@ +package xyz.zhouxy.plusone.spring; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +public class SpringContextHolder { + + private ApplicationContext context; + + private static final SpringContextHolder INSTANCE = new SpringContextHolder(); + + private SpringContextHolder() { + } + + public static ApplicationContext getContext() { + return INSTANCE.context; + } + + @Configuration + static class SpringContextHolderConfig { + @Bean(name = "springContextHolder") + SpringContextHolder getSpringContextHolder(ApplicationContext context) { + SpringContextHolder.INSTANCE.context = context; + return SpringContextHolder.INSTANCE; + } + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/sql/SQL.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/sql/SQL.java new file mode 100644 index 0000000..df2867c --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/sql/SQL.java @@ -0,0 +1,34 @@ +package xyz.zhouxy.plusone.sql; + +import java.util.Objects; + +import org.apache.ibatis.jdbc.AbstractSQL; + +/** + * 扩展 MyBatis 中的 SQL 语句构造器 + * + * @author ZhouXY + */ +public class SQL extends AbstractSQL { + + public SQL SET_IF(boolean condition, String sets) { + return condition ? SET(sets) : getSelf(); + } + + public SQL SET_IF_NOT_NULL(Object param, String sets) { + return Objects.nonNull(param) ? SET(sets) : getSelf(); + } + + public SQL WHERE_IF(boolean condition, String sqlCondition) { + return condition ? WHERE(sqlCondition) : getSelf(); + } + + public SQL WHERE_IF_NOT_NULL(Object param, String sqlCondition) { + return Objects.nonNull(param) ? WHERE(sqlCondition) : getSelf(); + } + + @Override + public SQL getSelf() { + return this; + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/BaseValidator.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/BaseValidator.java new file mode 100644 index 0000000..36b13f6 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/BaseValidator.java @@ -0,0 +1,72 @@ +package xyz.zhouxy.plusone.validator; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +import xyz.zhouxy.plusone.exception.InvalidInputException; + +/** + * 校验器 + * + *

可以使用以下方式初始化一个校验器:

+ * + *
+ * BaseValidator<Integer> validator = new BaseValidator<>() {
+ *     {
+ *         ruleFor(value -> Objects.nonNull(value), "value 不能为空");
+ *         ruleFor(value -> (value >= 0 && value <= 500), "value 应在 [0, 500] 内");
+ *     }
+ * };
+ * 
+ * + *

也可以通过继承本类,定义一个校验器(可使用单例模式)。

+ * + *

+ * 然后通过校验器的 {@link #validate} 方法,或 + * {@link ValidateUtil#validate(Object, Validator)} 对指定对象进行校验。 + *

+ * + *
+ * ValidateUtil.validate(255, validator);
+ * 
+ * + *
+ * validator.validate(666);
+ * 
+ *

+ * + * @author ZhouXY + * @see IValidateRequired + * @see ValidateUtil + * @see Validator + */ +public abstract class BaseValidator { + + private final List> rules = new ArrayList<>(); + + protected BaseValidator() { + } + + protected final void ruleFor(Predicate rule, String errorMessage) { + this.rules.add(new RuleInfo<>(rule, errorMessage)); + } + + public void validate(T obj) { + this.rules.forEach((RuleInfo ruleInfo) -> { + if (!ruleInfo.rule.test(obj)) { + throw new InvalidInputException(ruleInfo.message); + } + }); + } + + protected static class RuleInfo { + Predicate rule; + String message; + + public RuleInfo(Predicate rule, String message) { + this.rule = rule; + this.message = message; + } + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/BaseValidator2.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/BaseValidator2.java new file mode 100644 index 0000000..b7ba736 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/BaseValidator2.java @@ -0,0 +1,129 @@ +package xyz.zhouxy.plusone.validator; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Predicate; + +import cn.hutool.core.exceptions.ValidateException; +import lombok.AllArgsConstructor; +import xyz.zhouxy.plusone.constant.RegexConsts; +import xyz.zhouxy.plusone.util.RegexUtil; + +public abstract class BaseValidator2 { + + private List> hs = new ArrayList<>(); + + protected final ValidValueHolder ruleFor(Function getter) { + ValidValueHolder validValueHolder = new ValidValueHolder<>(getter); + hs.add(validValueHolder); + return validValueHolder; + } + + public void validate(T obj) { + for (var holder : hs) { + var value = holder.getter.apply(obj); + for (var rule : holder.rules) { + if (!rule.condition.test(value)) { + throw new ValidateException(rule.errMsg); + } + } + } + } +} + +class ValidValueHolder { + Function getter; + + List> rules = new ArrayList<>(); + + public ValidValueHolder(Function getter) { + this.getter = getter; + } + + private void addRule(Predicate condition, String errMsg) { + this.rules.add(new RuleInfo<>(condition, errMsg)); + } + + public ValidValueHolder nonNull(String errMsg) { + addRule(Objects::nonNull, errMsg); + return this; + } + + public ValidValueHolder nonEmpty(String errMsg) { + addRule(value -> { + if (value == null) { + return false; + } + if (value instanceof Collection) { + return ((Collection) value).isEmpty(); + } + if (value instanceof String) { + return ((String) value).isEmpty(); + } + return false; + }, errMsg); + return this; + } + + public ValidValueHolder size(int min, int max, String errMsg) { + addRule(value -> { + if (value == null) { + return false; + } + if (value instanceof Collection) { + int size = ((Collection) value).size(); + return size >= min && size <= max; + } + return true; + }, errMsg); + return this; + } + + public ValidValueHolder length(int min, int max, String errMsg) { + addRule(value -> { + if (value == null) { + return false; + } + if (value instanceof String) { + int length = ((String) value).length(); + return length >= min && length <= max; + } + return true; + }, errMsg); + return this; + } + + public ValidValueHolder matches(String regex, String errMsg) { + addRule(input -> RegexUtil.matches(regex, (String) input), errMsg); + return this; + } + + public ValidValueHolder matchesOr(String[] regexs, String errMsg) { + addRule(input -> RegexUtil.matchesOr((String) input, regexs), errMsg); + return this; + } + + public ValidValueHolder matchesAnd(String[] regexs, String errMsg) { + addRule(input -> RegexUtil.matchesAnd((String) input, regexs), errMsg); + return this; + } + + public ValidValueHolder email(String errMsg) { + return matches(RegexConsts.EMAIL, errMsg); + } + + @SuppressWarnings("unchecked") + public ValidValueHolder and(Predicate condition, String errMsg) { + addRule((Predicate) condition, errMsg); + return this; + } + + @AllArgsConstructor + static final class RuleInfo { + Predicate condition; + String errMsg; + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/DtoValidator.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/DtoValidator.java new file mode 100644 index 0000000..fb0670b --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/DtoValidator.java @@ -0,0 +1,12 @@ +package xyz.zhouxy.plusone.validator; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface DtoValidator { + Class value(); +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/IValidateRequired.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/IValidateRequired.java new file mode 100644 index 0000000..82e98ae --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/IValidateRequired.java @@ -0,0 +1,13 @@ +package xyz.zhouxy.plusone.validator; + +/** + * 自带校验方法,校验不通过时直接抛异常。 + * + * @author ZhouXY + * + * @see ValidateUtil + * @see BaseValidator + */ +public interface IValidateRequired { + void validate(); +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/ValidateDto.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/ValidateDto.java new file mode 100644 index 0000000..ebf7a77 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/ValidateDto.java @@ -0,0 +1,42 @@ +package xyz.zhouxy.plusone.validator; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +@Aspect +@Component +class ValidateDtosConfig { + + final Map, BaseValidator> validatorMap = new ConcurrentHashMap<>(); + + ValidateDtosConfig(ApplicationContext context) { + Map beans = context.getBeansWithAnnotation(DtoValidator.class); + for (var validator : beans.values()) { + Class targetClass = validator.getClass().getAnnotation(DtoValidator.class).value(); + this.validatorMap.put(targetClass, (BaseValidator) validator); + } + } + + @Before("@annotation(ValidateDto) && args(dto)") + @SuppressWarnings("unchecked") + public void doValidateDto(T dto) { + BaseValidator validator = (BaseValidator) this.validatorMap.get(dto.getClass()); + if (validator != null) { + validator.validate(dto); + } + } +} + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidateDto { +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/ValidateUtil.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/ValidateUtil.java new file mode 100644 index 0000000..1a8438d --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/ValidateUtil.java @@ -0,0 +1,29 @@ +package xyz.zhouxy.plusone.validator; + +/** + * 校验工具类 + *

+ * 对 {@link IValidateRequired} 的实现类对象进行校验 + *

+ * + * @author ZhouXY + * + * @see BaseValidator + * @see Validator + * @see IValidateRequired + */ +public class ValidateUtil { + private ValidateUtil() { + throw new IllegalStateException("Utility class"); + } + + public static void validate(Object obj) { + if (obj instanceof IValidateRequired) { + ((IValidateRequired) obj).validate(); + } + } + + public static void validate(T obj, BaseValidator validator) { + validator.validate(obj); + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/Validator.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/Validator.java new file mode 100644 index 0000000..cc6890e --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/validator/Validator.java @@ -0,0 +1,42 @@ +package xyz.zhouxy.plusone.validator; + +import java.util.function.Predicate; + +/** + * 校验器 + * + *

+ * 可以使用以下方式初始化一个校验器: + *

+ * + *
+ * var validator = new Validator<Integer>()
+ *         .addRule(value -> Objects.nonNull(value), "value 不能为空")
+ *         .addRule(value -> (value >= 0 && value <= 500), "value 应在 [0, 500] 内");
+ * 
+ * + *

+ * 然后通过校验器的 {@link #validate} 方法,或 + * {@link ValidateUtil#validate(Object, Validator)} 对指定对象进行校验。 + *

+ * + *
+ * validator.validate(666);
+ * 
+ * + *
+ * ValidateUtil.validate(255, validator);
+ * 
+ *

+ * + * @author ZhouXY + * @see IValidateRequired + * @see ValidateUtil + * @see BaseValidator + */ +public final class Validator extends BaseValidator { + public final Validator addRule(final Predicate rule, final String errorMessage) { + ruleFor(rule, errorMessage); + return this; + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/web/config/WebCorsConfig.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/web/config/WebCorsConfig.java new file mode 100644 index 0000000..1b2f55a --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/web/config/WebCorsConfig.java @@ -0,0 +1,23 @@ +package xyz.zhouxy.plusone.web.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * 跨域配置 + * + * @author ZhouXY + */ +@Configuration +public class WebCorsConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns("*") + .allowCredentials(true) + .allowedMethods("GET", "POST", "OPTIONS", "PUT", "DELETE", "PATCH") + .maxAge(3600); + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/web/config/WebEnumMapConfig.java b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/web/config/WebEnumMapConfig.java new file mode 100644 index 0000000..07c16e5 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/java/xyz/zhouxy/plusone/web/config/WebEnumMapConfig.java @@ -0,0 +1,55 @@ +package xyz.zhouxy.plusone.web.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.format.FormatterRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * Spring MVC 配置。 + * + *

+ * 将与前后端交互的数据中的枚举转换为其整数值 + *

+ * + * @author ZhouXY + */ +@Configuration +public class WebEnumMapConfig implements WebMvcConfigurer { + @Override + public void addFormatters(FormatterRegistry registry) { + registry.addConverterFactory(new IntToEnumConverterFactory()); + registry.addConverterFactory(new StringToEnumConverterFactory()); + } +} + + +class IntToEnumConverterFactory implements ConverterFactory> { + @Override + public > Converter getConverter(Class targetType) { + return (Integer source) -> { + try { + T[] values = targetType.getEnumConstants(); + return values[source]; + } catch (IndexOutOfBoundsException e) { + throw new EnumConstantNotPresentException(targetType, Integer.toString(source)); + } + }; + } +} + +class StringToEnumConverterFactory implements ConverterFactory> { + @Override + public > Converter getConverter(Class targetType) { + return (String source) -> { + int index = Integer.parseInt(source); + try { + T[] values = targetType.getEnumConstants(); + return values[index]; + } catch (IndexOutOfBoundsException e) { + throw new EnumConstantNotPresentException(targetType, Integer.toString(index)); + } + }; + } +} diff --git a/plusone-basic/plusone-basic-infrastructure/src/main/resources/conf/plusone.properties b/plusone-basic/plusone-basic-infrastructure/src/main/resources/conf/plusone.properties new file mode 100644 index 0000000..100731b --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/main/resources/conf/plusone.properties @@ -0,0 +1,31 @@ +spring.application.name=${plusone.application.name} + +spring.mail.host=${plusone.mail.host} +spring.mail.username=${plusone.mail.from} +spring.mail.password=${plusone.mail.password} +spring.mail.default-encoding=UTF-8 +spring.mail.properties.mail.debug=${plusone.debug} + +server.port=${plusone.server.port} + +mybatis-plus.global-config.banner=false +# 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) +mybatis-plus.global-config.db-config.logic-delete-field=deleted +# 逻辑已删除值(默认为 1) +mybatis-plus.global-config.db-config.logic-delete-value=id +# 逻辑未删除值(默认为 0) +mybatis-plus.global-config.db-config.logic-not-delete-value=0 +mybatis-plus.configuration.default-enum-type-handler=org.apache.ibatis.type.EnumOrdinalTypeHandler + +# Sa-Token 配置 +sa-token.is-print=false +# token名称 (同时也是cookie名称) +sa-token.token-name=${plusone.application.name} +# 是否允许同一账号并发登录 (为true时允许一起登录, 为 false 时新登录挤掉旧登录) +sa-token.is-concurrent=true +# 在多人登录同一账号时,是否共用一个token (为 true 时所有登录共用一个 token, 为false时每次登录新建一个 token) +sa-token.is-share=true +sa-token.token-style=simple-uuid +sa-token.activity-timeout=1800 +sa-token.is-read-cookie=false +sa-token.is-log=${plusone.debug} diff --git a/plusone-basic/plusone-basic-infrastructure/src/test/java/xyz/zhouxy/plusone/validator/BaseValidator2Test.java b/plusone-basic/plusone-basic-infrastructure/src/test/java/xyz/zhouxy/plusone/validator/BaseValidator2Test.java new file mode 100644 index 0000000..6acdee4 --- /dev/null +++ b/plusone-basic/plusone-basic-infrastructure/src/test/java/xyz/zhouxy/plusone/validator/BaseValidator2Test.java @@ -0,0 +1,41 @@ +package xyz.zhouxy.plusone.validator; + +import org.junit.jupiter.api.Test; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import xyz.zhouxy.plusone.constant.RegexConsts; + +class BaseValidator2Test { + + @Test + void testValid() { + LoginCommand loginCommand = new LoginCommand("13169053215@qq.com", "8GouTDE53a", false); + LoginCommandValidator.INSTANCE.validate(loginCommand); + System.err.println(loginCommand); + } +} + +@Data +@NoArgsConstructor +@AllArgsConstructor +class LoginCommand { + private String account; + private String pwd; + private boolean rememberMe; +} + +class LoginCommandValidator extends BaseValidator2 { + + public static final LoginCommandValidator INSTANCE = new LoginCommandValidator(); + + private LoginCommandValidator() { + ruleFor(loginCommand -> loginCommand.getAccount()) + .nonNull("邮箱地址不能为空") + .matchesOr(new String[] { RegexConsts.EMAIL, RegexConsts.MOBILE_PHONE }, "请输入邮箱地址或手机号"); + ruleFor(loginCommand -> loginCommand.getPwd()) + .nonNull("密码不能为空") + .matches(RegexConsts.PASSWORD, "密码格式错误"); + } +} diff --git a/plusone-basic/pom.xml b/plusone-basic/pom.xml new file mode 100644 index 0000000..6072631 --- /dev/null +++ b/plusone-basic/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + plusone + xyz.zhouxy + 1.0.0-SNAPSHOT + + + xyz.zhouxy + plusone-basic + 1.0.0-SNAPSHOT + + pom + + plusone-basic + + + plusone-basic-common + plusone-basic-domain + plusone-basic-infrastructure + plusone-basic-application + + + + + + xyz.zhouxy + plusone-basic-common + ${plusone-basic.version} + + + xyz.zhouxy + plusone-basic-domain + ${plusone-basic.version} + + + xyz.zhouxy + plusone-basic-infrastructure + ${plusone-basic.version} + + + xyz.zhouxy + plusone-basic-application + ${plusone-basic.version} + + + + + + + org.junit.jupiter + junit-jupiter-api + test + + + diff --git a/plusone-start/pom.xml b/plusone-start/pom.xml new file mode 100644 index 0000000..e5bdcb4 --- /dev/null +++ b/plusone-start/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + + plusone + xyz.zhouxy + 1.0.0-SNAPSHOT + + + xyz.zhouxy + plusone-start + 1.0.0-SNAPSHOT + + plusone-start + + 参考 DDD 落地的脚手架 + + + 2.7.6 + + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + xyz.zhouxy + plusone-system-application + ${plusone-system.version} + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${maven.compiler.source} + ${maven.compiler.target} + UTF-8 + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + xyz.zhouxy.plusone.PlusoneApplication + + + + repackage + + repackage + + + + + + + + diff --git a/plusone-start/src/main/java/xyz/zhouxy/plusone/PlusoneApplication.java b/plusone-start/src/main/java/xyz/zhouxy/plusone/PlusoneApplication.java new file mode 100644 index 0000000..bfb3c8a --- /dev/null +++ b/plusone-start/src/main/java/xyz/zhouxy/plusone/PlusoneApplication.java @@ -0,0 +1,17 @@ +package xyz.zhouxy.plusone; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import lombok.extern.slf4j.Slf4j; + +@SpringBootApplication +@Slf4j +public class PlusoneApplication { + + public static void main(String[] args) { + log.debug("Plusone started!"); + SpringApplication.run(PlusoneApplication.class, args); + } + +} diff --git a/plusone-start/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/plusone-start/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000..d65dd7f --- /dev/null +++ b/plusone-start/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,34 @@ +{ + "properties": [ + { + "name": "plusone.application.name", + "type": "java.lang.String", + "description": "A description for 'plusone.application.name'" + }, + { + "name": "plusone.server.port", + "type": "java.lang.Integer", + "description": "A description for 'plusone.server.port'" + }, + { + "name": "plusone.debug", + "type": "java.lang.Boolean", + "description": "A description for 'plusone.debug'" + }, + { + "name": "plusone.mail.host", + "type": "java.lang.String", + "description": "A description for 'plusone.mail.host'" + }, + { + "name": "plusone.mail.password", + "type": "java.lang.String", + "description": "A description for 'plusone.mail.password'" + }, + { + "name": "plusone.exception.handle-all-exception", + "type": "java.lang.Boolean", + "description": "A description for 'plusone.exception.handle-all-exception'" + } + ] +} diff --git a/plusone-start/src/main/resources/application.yaml b/plusone-start/src/main/resources/application.yaml new file mode 100644 index 0000000..80c6303 --- /dev/null +++ b/plusone-start/src/main/resources/application.yaml @@ -0,0 +1,79 @@ +plusone: + application: + name: plusone + server: + port: 8108 + debug: true + + # 短信发送相关参数 + sms: + region: ap-guangzhou + credential: + secret-id: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + secret-key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + client: + # SDK 默认用 TC3-HMAC-SHA256 进行签名,非必要请不要修改这个字段 + sign-method: HmacSHA256 + http: + # proxy: + # host: xxx + # port: xxx + req-method: POST + conn-timeout: 60 + app-id: 1111111111 + templates: + code: 0000000 + + # 邮件发送相关参数 + mail: + host: smtp.163.com + #设置邮件发送者 + from: example@163.com + password: XXXXXXXXXXXXXXXX + subject: + code: Plusone + template: + code: 【Plusone】验证码:%s,10分钟内有效,请勿泄露。 + + # 异常拦截机制是否拦截所有异常 + exception: + handle-all-exception: false + +spring: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 1 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + # password: + # 连接超时时间 + timeout: 10s + lettuce: + pool: + # 连接池最大连接数 + max-active: 200 + # 连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + # 连接池中的最大空闲连接 + max-idle: 10 + # 连接池中的最小空闲连接 + min-idle: 0 + # 数据库 + datasource: + url: jdbc:postgresql://localhost:5432/plusone + username: plusone + password: XXXXXXXXXXXXXXXX + +# 日志配置 +logging: + file: + name: ${user.home}/logs/${plusone.application.name}.log + level: + root: INFO + xyz.zhouxy.plusone: DEBUG + org.springframework.jdbc.core: DEBUG + xyz.zhouxy.plusone.system.application.query: DEBUG diff --git a/plusone-start/src/main/resources/logback.xml b/plusone-start/src/main/resources/logback.xml new file mode 100644 index 0000000..de3d0d4 --- /dev/null +++ b/plusone-start/src/main/resources/logback.xml @@ -0,0 +1,32 @@ + + + + + + + + + %green(%d{yyyy-MM-dd HH:mm:ss.SSS}) %highlight(%5level) ${PID:- } - [%15.15t] %cyan(%-40.108logger) : %msg%n + + + + + + + + + + + diff --git a/plusone-start/src/test/java/xyz/zhouxy/plusone/SerialTests.java b/plusone-start/src/test/java/xyz/zhouxy/plusone/SerialTests.java new file mode 100644 index 0000000..436ecb6 --- /dev/null +++ b/plusone-start/src/test/java/xyz/zhouxy/plusone/SerialTests.java @@ -0,0 +1,22 @@ +package xyz.zhouxy.plusone; + +import java.io.ObjectStreamClass; + +import org.junit.jupiter.api.Test; + +import lombok.extern.slf4j.Slf4j; +import xyz.zhouxy.plusone.exception.PlusoneException; + +@Slf4j +class SerialTests { + + @Test + void testSerialVersionUID() { + var cl = PlusoneException.class; + var c = ObjectStreamClass.lookup(cl); + var uid = c.getSerialVersionUID(); + log.info("\n @java.io.Serial" + + "\n private static final long serialVersionUID = {}L;", uid); + } + +} diff --git a/plusone-start/src/test/java/xyz/zhouxy/plusone/TestAop.java b/plusone-start/src/test/java/xyz/zhouxy/plusone/TestAop.java new file mode 100644 index 0000000..12a2322 --- /dev/null +++ b/plusone-start/src/test/java/xyz/zhouxy/plusone/TestAop.java @@ -0,0 +1,31 @@ +package xyz.zhouxy.plusone; + +import javax.annotation.Resource; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import xyz.zhouxy.plusone.system.application.query.result.LoginInfoViewObject; +import xyz.zhouxy.plusone.system.application.service.AdminLoginService; +import xyz.zhouxy.plusone.system.application.service.command.LoginByPasswordCommand; +import xyz.zhouxy.plusone.system.domain.model.account.AccountRepository; + +@SpringBootTest(classes = PlusoneApplication.class) +class TestAop { + + @Resource + AccountRepository repository; + + @Resource + AdminLoginService service; + + @Test + void testAop() { + LoginByPasswordCommand command = new LoginByPasswordCommand(); + command.setPrincipal("Code108"); + command.setPassword("2333"); + command.setRememberMe(false); + LoginInfoViewObject loginInfo = service.loginByPassword(command); + System.err.println(loginInfo); + } +} diff --git a/plusone-system/plusone-system-application/pom.xml b/plusone-system/plusone-system-application/pom.xml new file mode 100644 index 0000000..fca6a11 --- /dev/null +++ b/plusone-system/plusone-system-application/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + plusone-system + xyz.zhouxy + 1.0.0-SNAPSHOT + + + plusone-system-application + + + + xyz.zhouxy + plusone-system-domain + + + xyz.zhouxy + plusone-system-infrastructure + + + xyz.zhouxy + plusone-basic-application + + + + diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/common/util/MenuUtil.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/common/util/MenuUtil.java new file mode 100644 index 0000000..55f0918 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/common/util/MenuUtil.java @@ -0,0 +1,61 @@ +package xyz.zhouxy.plusone.system.application.common.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import xyz.zhouxy.plusone.domain.IWithOrderNumber; +import xyz.zhouxy.plusone.system.application.query.result.MenuViewObject; +import xyz.zhouxy.plusone.system.domain.model.menu.Menu.MenuType; + +/** + * 菜单工具类 + * + * @author ZhouXY + */ +public class MenuUtil { + private MenuUtil() { + throw new IllegalStateException("Utility class"); + } + + /** + * 构建菜单树 + * + * @param allMenus 菜单列表 + * @return 菜单树 + */ + public static List buildMenuTree(Collection allMenus) { + // 先排序,保证添加到 rootMenus 中的顺序,以及 addChild 添加的子菜单的顺序 + allMenus = allMenus.stream() + .sorted(Comparator.comparing(IWithOrderNumber::getOrderNumber)) + .toList(); + + // 一级菜单 + List rootMenus = new ArrayList<>(); + // key: 菜单 id; value: 菜单对象. 方便根据 id 查找相应对象。 + Map menuListMap = new HashMap<>(); + + for (var menu : allMenus) { + // 添加 MENU_LIST 到 map 中,方便后面调用对象的方法 + if (menu.getType() == MenuType.MENU_LIST.ordinal()) { + menuListMap.put(menu.getId(), menu); + } + // 一级菜单 + if (menu.getParentId() == 0) { + rootMenus.add(menu); + } + } + for (var menu : allMenus) { + var parent = menuListMap.getOrDefault(menu.getParentId(), null); + // 父菜单存在于 map 中,调用父菜单的 addChild 方法将当前菜单添加为父菜单的子菜单。 + if (parent != null) { + parent.addChild(menu); + } + } + + return rootMenus; + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/common/util/PrincipalType.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/common/util/PrincipalType.java new file mode 100644 index 0000000..14022c7 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/common/util/PrincipalType.java @@ -0,0 +1,18 @@ +package xyz.zhouxy.plusone.system.application.common.util; + +import lombok.Getter; +import xyz.zhouxy.plusone.constant.RegexConsts; + +public enum PrincipalType { + EMAIL(RegexConsts.EMAIL), + MOBILE_PHONE(RegexConsts.MOBILE_PHONE), + USERNAME(RegexConsts.USERNAME) + ; + + @Getter + private final String regex; + + PrincipalType(String regex) { + this.regex = regex; + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/common/util/PrincipalUtil.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/common/util/PrincipalUtil.java new file mode 100644 index 0000000..6e820f7 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/common/util/PrincipalUtil.java @@ -0,0 +1,64 @@ +package xyz.zhouxy.plusone.system.application.common.util; + +import javax.annotation.Nullable; + +import xyz.zhouxy.plusone.exception.InvalidInputException; +import xyz.zhouxy.plusone.system.domain.model.account.Email; +import xyz.zhouxy.plusone.system.domain.model.account.MobilePhone; +import xyz.zhouxy.plusone.system.domain.model.account.Principal; +import xyz.zhouxy.plusone.system.domain.model.account.Username; + +/** + * 根据字面值,判断并生成 {@link Principal} 值对象。 + * + * @author ZhouXY + * @see Principal + * @see Username + * @see Email + * @see MobilePhone + * @see InvalidInputException + */ +public class PrincipalUtil { + + private PrincipalUtil() { + throw new IllegalStateException("Utility class"); + } + + public static PrincipalType getPrincipalType(@Nullable String principal) { + if (principal == null) { + throw new IllegalArgumentException("principal 不能为空"); + } + PrincipalType[] principalTypes = PrincipalType.values(); + for (var principalType : principalTypes) { + if (principal.matches(principalType.getRegex())) { + return principalType; + } + } + throw InvalidInputException.unsupportedPrincipalTypeException(); + } + + public static Principal getPrincipal(@Nullable String principal) { + PrincipalType principalType = getPrincipalType(principal); + if (principalType == PrincipalType.EMAIL) { + return Email.of(principal); + } + if (principalType == PrincipalType.MOBILE_PHONE) { + return MobilePhone.of(principal); + } + if (principalType == PrincipalType.USERNAME) { + return Username.of(principal); + } + throw InvalidInputException.unsupportedPrincipalTypeException(); + } + + public static Principal getEmailOrMobilePhone(@Nullable String principal) { + PrincipalType principalType = getPrincipalType(principal); + if (principalType == PrincipalType.EMAIL) { + return Email.of(principal); + } + if (principalType == PrincipalType.MOBILE_PHONE) { + return MobilePhone.of(principal); + } + throw InvalidInputException.unsupportedPrincipalTypeException("输入邮箱地址或手机号"); + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/AccountContextController.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/AccountContextController.java new file mode 100644 index 0000000..b1aa866 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/AccountContextController.java @@ -0,0 +1,40 @@ +package xyz.zhouxy.plusone.system.application.controller; + +import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import xyz.zhouxy.plusone.system.application.service.AccountContextService; +import xyz.zhouxy.plusone.util.RestfulResult; + +/** + * 账号查询本身相关信息 + * + * @author ZhouXY + */ +@RestController +@RequestMapping("account") +public class AccountContextController { + + private final AccountContextService service; + + public AccountContextController(AccountContextService service) { + this.service = service; + } + + @GetMapping("info") + public RestfulResult getAccountInfo() { + adminAuthLogic.checkLogin(); + var result = service.getAccountInfo(); + return RestfulResult.success("查询成功", result); + } + + @GetMapping("menus") + public RestfulResult getMenuTree() { + adminAuthLogic.checkLogin(); + var result = service.getMenuTree(); + return RestfulResult.success("查询成功", result); + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/AccountManagementController.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/AccountManagementController.java new file mode 100644 index 0000000..8699d3d --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/AccountManagementController.java @@ -0,0 +1,83 @@ +package xyz.zhouxy.plusone.system.application.controller; + +import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic; +import static xyz.zhouxy.plusone.util.RestfulResult.success; + +import java.util.List; + +import javax.validation.Valid; + +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import xyz.zhouxy.plusone.system.application.query.params.AccountQueryParams; +import xyz.zhouxy.plusone.system.application.service.AccountManagementService; +import xyz.zhouxy.plusone.system.application.service.command.CreateAccountCommand; +import xyz.zhouxy.plusone.system.application.service.command.UpdateAccountCommand; +import xyz.zhouxy.plusone.util.AssertResult; +import xyz.zhouxy.plusone.util.RestfulResult; + +/** + * 账号管理 + * + * @author ZhouXY + */ +@RestController +@RequestMapping("sys/account") +public class AccountManagementController { + + private final AccountManagementService service; + + public AccountManagementController(AccountManagementService service) { + this.service = service; + } + + @PostMapping + public RestfulResult createAccount(@RequestBody @Valid CreateAccountCommand command) { + adminAuthLogic.checkLogin(); + adminAuthLogic.checkPermission("sys-account-create"); + service.createAccount(command); + return success(); + } + + @DeleteMapping + public RestfulResult deleteAccounts(@RequestBody List ids) { + adminAuthLogic.checkLogin(); + adminAuthLogic.checkPermission("sys-account-delete"); + service.deleteAccounts(ids); + return success(); + } + + @PatchMapping("{id}") + public RestfulResult updateAccountInfo( + @PathVariable("id") Long id, + @RequestBody @Valid UpdateAccountCommand command) { + adminAuthLogic.checkLogin(); + adminAuthLogic.checkPermission("sys-account-update"); + service.updateAccountInfo(id, command); + return success(); + } + + @GetMapping("query") + public RestfulResult queryAccountOverviewList(AccountQueryParams queryParams) { + adminAuthLogic.checkLogin(); + adminAuthLogic.checkPermission("sys-account-list"); + var accountOverviewList = service.queryAccountOverviewList(queryParams); + return success("查询成功", accountOverviewList); + } + + @GetMapping("{accountId}") + public RestfulResult queryAccountDetails(@PathVariable("accountId") Long accountId) { + adminAuthLogic.checkLogin(); + adminAuthLogic.checkPermission("sys-account-details"); + var accountDetails = service.queryAccountDetails(accountId); + AssertResult.nonNull(accountDetails); + return success("查询成功", accountDetails); + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/AdminLoginController.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/AdminLoginController.java new file mode 100644 index 0000000..cb41b85 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/AdminLoginController.java @@ -0,0 +1,49 @@ +package xyz.zhouxy.plusone.system.application.controller; + +import static xyz.zhouxy.plusone.util.RestfulResult.success; + +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import xyz.zhouxy.plusone.system.application.service.AdminLoginService; +import xyz.zhouxy.plusone.system.application.service.command.LoginByOtpCommand; +import xyz.zhouxy.plusone.system.application.service.command.LoginByPasswordCommand; +import xyz.zhouxy.plusone.util.RestfulResult; + +/** + * Admin 账号登录 + * + * @author ZhouXY + */ +@RestController +@RequestMapping("login") +public class AdminLoginController { + + private final AdminLoginService service; + + public AdminLoginController(AdminLoginService service) { + this.service = service; + } + + @PostMapping("byPassword") + public RestfulResult loginByPassword(@RequestBody LoginByPasswordCommand command) { + var loginInfo = service.loginByPassword(command); + return success("登录成功", loginInfo); + } + + @PostMapping("byOtp") + public RestfulResult loginByOtp(@RequestBody LoginByOtpCommand command) { + var loginInfo = service.loginByOtp(command); + return success("登录成功", loginInfo); + } + + @GetMapping("sendOtp") + public RestfulResult sendOtp(@RequestParam String principal) { + service.sendOtp(principal); + return success("发送成功"); + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/AdminLogoutController.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/AdminLogoutController.java new file mode 100644 index 0000000..a7c1a81 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/AdminLogoutController.java @@ -0,0 +1,30 @@ +package xyz.zhouxy.plusone.system.application.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import xyz.zhouxy.plusone.system.application.service.AdminLogoutService; +import xyz.zhouxy.plusone.util.RestfulResult; + +/** + * Admin 账号登出 + * + * @author ZhouXY + */ +@RestController +@RequestMapping("logout") +public class AdminLogoutController { + + private final AdminLogoutService service; + + public AdminLogoutController(AdminLogoutService service) { + this.service = service; + } + + @GetMapping + public RestfulResult execute() { + service.execute(); + return RestfulResult.success("注销成功"); + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/DictManagementController.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/DictManagementController.java new file mode 100644 index 0000000..1968225 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/DictManagementController.java @@ -0,0 +1,83 @@ +package xyz.zhouxy.plusone.system.application.controller; + +import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic; +import static xyz.zhouxy.plusone.util.RestfulResult.success; + +import java.util.List; + +import javax.validation.Valid; + +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import xyz.zhouxy.plusone.system.application.query.params.DictQueryParams; +import xyz.zhouxy.plusone.system.application.service.DictManagementService; +import xyz.zhouxy.plusone.system.application.service.command.CreateDictCommand; +import xyz.zhouxy.plusone.system.application.service.command.UpdateDictCommand; +import xyz.zhouxy.plusone.util.RestfulResult; + +/** + * 数据字典管理 + * + * @author ZhouXY + */ +@RestController +@RequestMapping("sys/dict") +public class DictManagementController { + + private final DictManagementService service; + + public DictManagementController(DictManagementService service) { + this.service = service; + } + + @PostMapping + public RestfulResult createDict(@RequestBody @Valid CreateDictCommand command) { + adminAuthLogic.checkPermission("sys-dict-create"); + service.createDict(command); + return success(); + } + + @DeleteMapping + public RestfulResult deleteDicts(@RequestBody List ids) { + adminAuthLogic.checkPermission("sys-dict-delete"); + service.deleteDicts(ids); + return success(); + } + + @PatchMapping("{id}") + public RestfulResult updateDict( + @PathVariable("id") Long id, + @RequestBody @Valid UpdateDictCommand command) { + adminAuthLogic.checkPermission("sys-dict-update"); + service.updateDict(id, command); + return success(); + } + + @GetMapping("{dictId}") + public RestfulResult findDictDetails(@PathVariable("dictId") Long dictId) { + adminAuthLogic.checkPermission("sys-dict-details"); + var dictDetails = service.findDictDetails(dictId); + return success("查询成功", dictDetails); + } + + @GetMapping("all") + public RestfulResult loadAllDicts() { + adminAuthLogic.checkPermissionAnd("sys-dict-list", "sys-dict-details"); + var dicts = service.loadAllDicts(); + return success("查询成功", dicts); + } + + @GetMapping("query") + public RestfulResult queryDictOverviewList(@Valid DictQueryParams queryParams) { + adminAuthLogic.checkPermission("sys-dict-list"); + var dicts = service.queryDictOverviewList(queryParams); + return success("查询成功", dicts); + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/MenuManagementController.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/MenuManagementController.java new file mode 100644 index 0000000..e2b55ad --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/MenuManagementController.java @@ -0,0 +1,85 @@ +package xyz.zhouxy.plusone.system.application.controller; + +import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic; +import static xyz.zhouxy.plusone.util.RestfulResult.success; + +import javax.validation.Valid; + +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import xyz.zhouxy.plusone.system.application.service.MenuManagementService; +import xyz.zhouxy.plusone.system.application.service.command.CreateMenuCommand; +import xyz.zhouxy.plusone.system.application.service.command.UpdateMenuCommand; +import xyz.zhouxy.plusone.util.RestfulResult; + +/** + * 菜单管理 + * + * @author ZhouXY + */ +@RestController +@RequestMapping("sys/menu") +public class MenuManagementController { + + private final MenuManagementService service; + + public MenuManagementController(MenuManagementService service) { + this.service = service; + } + + // ==================== create ==================== + @PostMapping + public RestfulResult createMenu(@RequestBody @Valid CreateMenuCommand command) { + adminAuthLogic.checkPermission("sys-menu-create"); + service.createMenu(command); + return success(); + } + + // ==================== delete ==================== + @DeleteMapping("{id}") + public RestfulResult deleteMenu(@PathVariable("id") Long id) { + adminAuthLogic.checkPermission("sys-menu-delete"); + service.deleteMenu(id); + return success(); + } + + // ==================== update ==================== + @PatchMapping("{id}") + public RestfulResult updateMenu( + @PathVariable("id") Long id, + @RequestBody @Valid UpdateMenuCommand command) { + adminAuthLogic.checkPermission("sys-menu-update"); + service.updateMenu(id, command); + return success(); + } + + // ==================== query ==================== + @GetMapping("{id}") + public RestfulResult findById(@PathVariable("id") Long id) { + adminAuthLogic.checkPermission("sys-menu-details"); + var result = service.findById(id); + return RestfulResult.success("查询成功", result); + } + + @GetMapping("queryByAccountId") + public RestfulResult queryByAccountId(@RequestParam Long accountId) { + adminAuthLogic.checkPermission("sys-menu-details"); + var result = service.queryByAccountId(accountId); + return success("查询成功", result); + } + + @GetMapping("queryByRoleId") + public RestfulResult queryByRoleId(@RequestParam Long roleId) { + adminAuthLogic.checkPermission("sys-menu-details"); + var result = service.queryByRoleId(roleId); + return success("查询成功", result); + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/RegisterAccountController.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/RegisterAccountController.java new file mode 100644 index 0000000..b762370 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/RegisterAccountController.java @@ -0,0 +1,42 @@ +package xyz.zhouxy.plusone.system.application.controller; + +import static xyz.zhouxy.plusone.util.RestfulResult.success; + +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import xyz.zhouxy.plusone.system.application.service.RegisterAccountService; +import xyz.zhouxy.plusone.system.application.service.command.RegisterAccountCommand; +import xyz.zhouxy.plusone.util.RestfulResult; + +/** + * 注册账号服务 + * + * @author ZhouXY + */ +@RestController +@RequestMapping("register") +public class RegisterAccountController { + + private final RegisterAccountService service; + + public RegisterAccountController(RegisterAccountService service) { + this.service = service; + } + + @PostMapping + public RestfulResult registerAccount(@RequestBody RegisterAccountCommand command) { + service.registerAccount(command); + return success("注册成功"); + } + + @GetMapping("sendCode") + public RestfulResult sendCode(@RequestParam String principal) { + service.sendCode(principal); + return success("发送成功"); + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/RoleManagementController.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/RoleManagementController.java new file mode 100644 index 0000000..aaf34e6 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/controller/RoleManagementController.java @@ -0,0 +1,78 @@ +package xyz.zhouxy.plusone.system.application.controller; + +import javax.validation.Valid; +import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic; + +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import xyz.zhouxy.plusone.system.application.query.params.RoleQueryParams; +import xyz.zhouxy.plusone.system.application.service.RoleManagementService; +import xyz.zhouxy.plusone.system.application.service.command.CreateRoleCommand; +import xyz.zhouxy.plusone.system.application.service.command.UpdateRoleCommand; +import xyz.zhouxy.plusone.util.RestfulResult; + +/** + * 角色管理服务 + * + * @author ZhouXY + */ +@RestController +@RequestMapping("sys/role") +public class RoleManagementController { + + private final RoleManagementService service; + + public RoleManagementController(RoleManagementService service) { + this.service = service; + } + + @PostMapping + public RestfulResult createRole(@RequestBody @Valid CreateRoleCommand command) { + adminAuthLogic.checkPermission("sys-role-create"); + service.createRole(command); + return RestfulResult.success(); + } + + @PatchMapping + public RestfulResult updateRole(@RequestBody @Valid UpdateRoleCommand command) { + adminAuthLogic.checkPermission("sys-role-update"); + service.updateRole(command); + return RestfulResult.success(); + } + + @DeleteMapping("{id}") + public RestfulResult delete(@PathVariable("id") Long id) { + adminAuthLogic.checkPermission("sys-role-delete"); + service.delete(id); + return RestfulResult.success(); + } + + @GetMapping("exists") + public RestfulResult exists(@RequestParam("id") Long id) { + adminAuthLogic.checkPermissionOr("sys-role-list", "sys-role-details"); + var isExists = service.exists(id); + return RestfulResult.success(isExists ? "存在" : "不存在", isExists); + } + + @GetMapping("{id}") + public RestfulResult findById(@PathVariable("id") Long id) { + adminAuthLogic.checkPermission("sys-role-details"); + var result = service.findById(id); + return RestfulResult.success("查询成功", result); + } + + @GetMapping("query") + public RestfulResult query(RoleQueryParams params) { + adminAuthLogic.checkPermission("sys-role-list"); + var result = service.query(params); + return RestfulResult.success("查询成功", result); + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/exception/AccountLoginException.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/exception/AccountLoginException.java new file mode 100644 index 0000000..8edc3e2 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/exception/AccountLoginException.java @@ -0,0 +1,38 @@ +package xyz.zhouxy.plusone.system.application.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +import xyz.zhouxy.plusone.exception.PlusoneException; + +@ResponseStatus(HttpStatus.BAD_REQUEST) +public class AccountLoginException extends PlusoneException { + @java.io.Serial + private static final long serialVersionUID = -3040996790739138556L; + + private static final int DEFAULT_ERR_CODE = 4030000; + + private AccountLoginException() { + super(DEFAULT_ERR_CODE, "用户登录异常"); + } + + private AccountLoginException(int code, String message) { + super(code, message); + } + + public static AccountLoginException accountNotExistException() { + return new AccountLoginException(4030101, "用户账户不存在"); + } + + public static AccountLoginException otpErrorException() { + return new AccountLoginException(4030501, "验证码错误"); + } + + public static AccountLoginException otpNotExistsException() { + return new AccountLoginException(4030502, "验证码不存在或已过期"); + } + + public static AccountLoginException passwordErrorException() { + return new AccountLoginException(4030200, "用户密码错误"); + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/exception/AccountRegisterException.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/exception/AccountRegisterException.java new file mode 100644 index 0000000..9ed0900 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/exception/AccountRegisterException.java @@ -0,0 +1,57 @@ +package xyz.zhouxy.plusone.system.application.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +import xyz.zhouxy.plusone.exception.PlusoneException; + +@ResponseStatus(HttpStatus.BAD_REQUEST) +public class AccountRegisterException extends PlusoneException { + + @java.io.Serial + private static final long serialVersionUID = 7580245181633370195L; + + public AccountRegisterException() { + this(4020000, "用户注册错误"); + } + + public AccountRegisterException(String message) { + this(4020000, message); + } + + public AccountRegisterException(Throwable cause) { + super(4020000, cause); + } + + public AccountRegisterException(int code, String message) { + super(code, message); + } + + public AccountRegisterException(int code, Throwable cause) { + super(code, cause); + } + + public static AccountRegisterException emailOrMobilePhoneRequiredException() { + return new AccountRegisterException(4020300, "邮箱和手机号应至少绑定一个"); + } + + public static AccountRegisterException usernameAlreadyExists(String username) { + return new AccountRegisterException(4020400, String.format("用户名 %s 已存在", username)); + } + + public static AccountRegisterException emailAlreadyExists(String value) { + return new AccountRegisterException(4020500, String.format("邮箱 %s 已存在", value)); + } + + public static AccountRegisterException mobilePhoneAlreadyExists(String value) { + return new AccountRegisterException(4020600, String.format("手机号 %s 已存在", value)); + } + + public static AccountRegisterException codeErrorException() { + return new AccountRegisterException(4020701, "校验码错误"); + } + + public static AccountRegisterException codeNotExistsException() { + return new AccountRegisterException(4020702, "校验码不存在或已过期"); + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/exception/UnsupportedMenuTypeException.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/exception/UnsupportedMenuTypeException.java new file mode 100644 index 0000000..993d936 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/exception/UnsupportedMenuTypeException.java @@ -0,0 +1,17 @@ +package xyz.zhouxy.plusone.system.application.exception; + +import xyz.zhouxy.plusone.exception.InvalidInputException; + +public class UnsupportedMenuTypeException extends InvalidInputException { + + @java.io.Serial + private static final long serialVersionUID = -769169844015637730L; + + public UnsupportedMenuTypeException() { + this("不支持的菜单类型"); + } + + public UnsupportedMenuTypeException(String message) { + super(message); + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/exception/handler/SaTokenExceptionHandler.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/exception/handler/SaTokenExceptionHandler.java new file mode 100644 index 0000000..79d91bf --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/exception/handler/SaTokenExceptionHandler.java @@ -0,0 +1,56 @@ +package xyz.zhouxy.plusone.system.application.exception.handler; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import cn.dev33.satoken.exception.DisableServiceException; +import cn.dev33.satoken.exception.SameTokenInvalidException; +import cn.dev33.satoken.exception.NotBasicAuthException; +import cn.dev33.satoken.exception.NotLoginException; +import cn.dev33.satoken.exception.NotPermissionException; +import cn.dev33.satoken.exception.NotRoleException; +import cn.dev33.satoken.exception.NotSafeException; +import cn.dev33.satoken.exception.SaTokenException; +import lombok.extern.slf4j.Slf4j; +import xyz.zhouxy.plusone.exception.handler.BaseExceptionHandler; +import xyz.zhouxy.plusone.util.RestfulResult; + +/** + * Sa-Token 异常处理器 + * + * @author ZhouXY + */ +@RestControllerAdvice +@Slf4j +public class SaTokenExceptionHandler extends BaseExceptionHandler { + + public SaTokenExceptionHandler(ExceptionInfoHolder exceptionInfoHolder) { + super(exceptionInfoHolder); + set(NotPermissionException.class, 4030103, "会话未能通过权限认证", HttpStatus.FORBIDDEN); + set(NotRoleException.class, 4030103, "会话未能通过角色认证", HttpStatus.FORBIDDEN); + set(DisableServiceException.class, 4030202, "账号指定服务已被封禁", HttpStatus.FORBIDDEN); + set(SameTokenInvalidException.class, 4030400, "提供的 Same-Token 无效", HttpStatus.UNAUTHORIZED); + set(NotBasicAuthException.class, 4030000, "会话未能通过 Http Basic 认证", HttpStatus.UNAUTHORIZED); + set(NotSafeException.class, 4020300, "会话未能通过二级认证", HttpStatus.UNAUTHORIZED); + set(NotLoginException.class, + 4020400, + e -> switch (((NotLoginException) e).getType()) { + case NotLoginException.NOT_TOKEN -> "未提供 Token"; + case NotLoginException.INVALID_TOKEN -> "Token 无效"; + case NotLoginException.TOKEN_TIMEOUT -> "Token 已过期"; + case NotLoginException.BE_REPLACED -> "Token 已被顶下线"; + case NotLoginException.KICK_OUT -> "Token 已被踢下线"; + default -> "当前会话未登录"; + }, + HttpStatus.UNAUTHORIZED); + set(SaTokenException.class, 4020300, "未通过身份认证或权限认证", HttpStatus.FORBIDDEN); + } + + @ExceptionHandler(SaTokenException.class) + public ResponseEntity handleSaTokenException(SaTokenException e) { + log.error(e.getMessage(), e); + return buildExceptionResponse(e); + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/AccountQueries.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/AccountQueries.java new file mode 100644 index 0000000..17ab42a --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/AccountQueries.java @@ -0,0 +1,32 @@ +package xyz.zhouxy.plusone.system.application.query; + +import java.util.List; + +import org.apache.ibatis.annotations.Mapper; + +import xyz.zhouxy.plusone.system.application.query.params.AccountQueryParams; +import xyz.zhouxy.plusone.system.application.query.result.AccountDetails; +import xyz.zhouxy.plusone.system.application.query.result.AccountOverview; +import xyz.zhouxy.plusone.util.PageDTO; + +/** + * 账号信息查询器 + * + * @author ZhouXY + */ +@Mapper +public interface AccountQueries { + + default PageDTO queryAccountOverviewPage(AccountQueryParams queryParams) { + List content = queryAccountOverview(queryParams); + long total = count(queryParams); + return PageDTO.of(content, total); + } + + List queryAccountOverview(AccountQueryParams queryParams); + + long count(AccountQueryParams queryParams); + + AccountDetails queryAccountDetails(Long accountId); + +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/DictQueries.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/DictQueries.java new file mode 100644 index 0000000..bb7a4c7 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/DictQueries.java @@ -0,0 +1,39 @@ +package xyz.zhouxy.plusone.system.application.query; + +import java.util.List; + +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Component; + +import xyz.zhouxy.plusone.sql.SQL; +import xyz.zhouxy.plusone.system.application.query.params.DictQueryParams; +import xyz.zhouxy.plusone.system.application.query.result.DictOverview; + +/** + * 数据字典查询器 + * + * @author ZhouXY + */ +@Component +public class DictQueries { + + private final NamedParameterJdbcTemplate jdbcTemplate; + + public DictQueries(NamedParameterJdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public List queryDictOverviewList(DictQueryParams queryParams) { + String sql = new SQL() + .SELECT("id", "dict_type", "dict_label", + "created_by", "create_time", "updated_by", "update_time", "count") + .FROM("view_sys_dict_overview") + .WHERE_IF(queryParams.getDictType() != null, "dict_type LIKE '%:dictType%'") + .WHERE_IF(queryParams.getDictLabel() != null, "dict_label LIKE '%:dictLabel%'") + .toString(); + return this.jdbcTemplate + .query(sql, new BeanPropertySqlParameterSource(queryParams), new BeanPropertyRowMapper<>(DictOverview.class)); + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/PermissionQueries.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/PermissionQueries.java new file mode 100644 index 0000000..4de7330 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/PermissionQueries.java @@ -0,0 +1,10 @@ +package xyz.zhouxy.plusone.system.application.query; + +/** + * + * + * @author ZhouXY + */ +public interface PermissionQueries { + // TODO【添加】 权限信息查询器 +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/RoleQueries.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/RoleQueries.java new file mode 100644 index 0000000..e1feef8 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/RoleQueries.java @@ -0,0 +1,64 @@ +package xyz.zhouxy.plusone.system.application.query; + +import java.util.List; + +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; + +import xyz.zhouxy.plusone.sql.SQL; +import xyz.zhouxy.plusone.system.application.query.params.RoleQueryParams; +import xyz.zhouxy.plusone.system.application.query.result.RoleOverview; + +/** + * 角色信息查询器 + * + * @author ZhouXY + */ +@Repository +public class RoleQueries { + + private final NamedParameterJdbcTemplate jdbcTemplate; + + public RoleQueries(NamedParameterJdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + /** + * 查询 Role + *

+ * !!!注意:此方法内存在字符串拼接,勿在循环内使用。 + *

+ * + * @param params 查询参数 + * @return 查询结果 + */ + public List query(RoleQueryParams params) { + String b = new SQL() + .SELECT("id") + .FROM("sys_role") + .WHERE_IF_NOT_NULL(params.getId(), "id = :id") + .WHERE_IF_NOT_NULL(params.getName(), "name = :name") + .WHERE_IF_NOT_NULL(params.getIdentifier(), "identifier = :identifier") + .WHERE_IF_NOT_NULL(params.getStatus(), "status = :status") + .WHERE_IF_NOT_NULL(params.getCreateTimeStart(), "create_time >= :createTimeStart") + .WHERE_IF_NOT_NULL(params.getCreateTimeEnd(), "create_time < :createTimeEnd") + .WHERE_IF_NOT_NULL(params.getUpdateTimeStart(), "update_time >= :updateTimeStart") + .WHERE_IF_NOT_NULL(params.getUpdateTimeEnd(), "update_time < :updateTimeEnd") + .LIMIT(params.getSize()) + .OFFSET(params.getOffset()) + .toString(); + var sql = """ + SELECT a.id AS id, a.name AS name, a.identifier AS identifier, a.status AS status + FROM sys_role AS a, ( + """ + b + """ + ) AS b + WHERE a.id = b.id + """; + return this.jdbcTemplate + .query(sql, new BeanPropertySqlParameterSource(params), + new BeanPropertyRowMapper<>(RoleOverview.class)); + } + +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/params/AccountQueryParams.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/params/AccountQueryParams.java new file mode 100644 index 0000000..40c127b --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/params/AccountQueryParams.java @@ -0,0 +1,55 @@ +package xyz.zhouxy.plusone.system.application.query.params; + +import java.time.LocalDate; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import xyz.zhouxy.plusone.util.PagingAndSortingQueryParams; + +/** + * 账号信息查询参数 + * + * @author ZhouXY + */ +@ToString +public class AccountQueryParams extends PagingAndSortingQueryParams { + + public AccountQueryParams() { + super("id", + "username", + "email", + "mobile_phone", + "status", + "nickname", + "sex", + "created_by", + "create_time", + "updated_by", + "update_time"); + } + + // TODO【添加】 注解参数校验 + private @Getter @Setter Long id; + private @Getter @Setter String username; + private @Getter @Setter String email; + private @Getter @Setter String mobilePhone; + private @Getter @Setter Integer status; + private @Getter @Setter String nickname; + private @Getter @Setter Integer sex; + private @Getter @Setter Long createdBy; + private @Getter @Setter LocalDate createTimeStart; + private @Getter LocalDate createTimeEnd; + private @Getter @Setter Long updatedBy; + private @Getter @Setter LocalDate updateTimeStart; + private @Getter LocalDate updateTimeEnd; + private @Getter @Setter Long roleId; + + public void setCreateTimeEnd(LocalDate createTimeEnd) { + this.createTimeEnd = createTimeEnd.plusDays(1); + } + + public void setUpdateTimeEnd(LocalDate updateTimeEnd) { + this.updateTimeEnd = updateTimeEnd.plusDays(1); + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/params/DictQueryParams.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/params/DictQueryParams.java new file mode 100644 index 0000000..7bd0dc3 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/params/DictQueryParams.java @@ -0,0 +1,21 @@ +package xyz.zhouxy.plusone.system.application.query.params; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; +import xyz.zhouxy.plusone.util.PagingAndSortingQueryParams; + +/** + * 数据字典查询参数 + * + * @author ZhouXY + */ +@Getter +@Setter +@Accessors(chain = true) +@ToString(callSuper = true) +public class DictQueryParams extends PagingAndSortingQueryParams { + String dictType; + String dictLabel; +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/params/RoleQueryParams.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/params/RoleQueryParams.java new file mode 100644 index 0000000..4b000e1 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/params/RoleQueryParams.java @@ -0,0 +1,34 @@ +package xyz.zhouxy.plusone.system.application.query.params; + +import java.time.LocalDate; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import xyz.zhouxy.plusone.util.PagingAndSortingQueryParams; + +/** + * 角色信息查询参数 + * + * @author ZhouXY + */ +@ToString(callSuper = true) +public class RoleQueryParams extends PagingAndSortingQueryParams { + private @Getter @Setter Long id; + private @Getter @Setter String name; + private @Getter @Setter String identifier; + private @Getter @Setter Integer status; + + private @Getter @Setter LocalDate createTimeStart; + private @Getter LocalDate createTimeEnd; + private @Getter @Setter LocalDate updateTimeStart; + private @Getter LocalDate updateTimeEnd; + + public void setCreateTimeEnd(LocalDate createTimeEnd) { + this.createTimeEnd = createTimeEnd.plusDays(1); + } + + public void setUpdateTimeEnd(LocalDate updateTimeEnd) { + this.updateTimeEnd = updateTimeEnd.plusDays(1); + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/AccountDetails.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/AccountDetails.java new file mode 100644 index 0000000..21da2eb --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/AccountDetails.java @@ -0,0 +1,29 @@ +package xyz.zhouxy.plusone.system.application.query.result; + +import java.util.Set; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +/** + * 账号详细信息 + * + * @author ZhouXY + */ +@Getter +@Setter +@NoArgsConstructor +@ToString +public class AccountDetails { + Long id; + String username; + String email; + String mobilePhone; + Integer status; + Set roles; + String nickname; + String avatar; + Integer sex; +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/AccountOverview.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/AccountOverview.java new file mode 100644 index 0000000..dbbb539 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/AccountOverview.java @@ -0,0 +1,38 @@ +package xyz.zhouxy.plusone.system.application.query.result; + +import java.time.LocalDateTime; +import java.util.Set; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +/** + * 账号概述信息 + * + * @author ZhouXY + */ +@Getter +@Setter +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class AccountOverview { + Long id; + String username; + String email; + String mobilePhone; + Integer status; + Set roles; + String nickname; + String avatar; + Integer sex; + Long createdBy; + LocalDateTime createTime; + Long updatedBy; + LocalDateTime updateTime; +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/DictOverview.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/DictOverview.java new file mode 100644 index 0000000..1388ccc --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/DictOverview.java @@ -0,0 +1,22 @@ +package xyz.zhouxy.plusone.system.application.query.result; + +import java.time.LocalDateTime; + +import lombok.Data; + +/** + * 数据字典概述信息 + * + * @author ZhouXY + */ +@Data +public class DictOverview { + Long id; + String dictType; + String dictLabel; + Integer count; + Long createdBy; + LocalDateTime createTime; + Long updatedBy; + LocalDateTime updateTime; +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/LoginInfoViewObject.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/LoginInfoViewObject.java new file mode 100644 index 0000000..0593a38 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/LoginInfoViewObject.java @@ -0,0 +1,24 @@ +package xyz.zhouxy.plusone.system.application.query.result; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +/** + * 登录结果 + * + * @author ZhouXY + */ +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@ToString +public class LoginInfoViewObject { + private String token; + private AccountDetails account; + + public static LoginInfoViewObject of(String token, AccountDetails accountDetails) { + return new LoginInfoViewObject(token, accountDetails); + } + +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/MenuViewObject.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/MenuViewObject.java new file mode 100644 index 0000000..316eafe --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/MenuViewObject.java @@ -0,0 +1,131 @@ +package xyz.zhouxy.plusone.system.application.query.result; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import xyz.zhouxy.plusone.domain.IWithOrderNumber; +import xyz.zhouxy.plusone.system.domain.model.menu.Action; +import xyz.zhouxy.plusone.system.domain.model.menu.Menu; +import xyz.zhouxy.plusone.system.domain.model.menu.Menu.MenuType; + +/** + * 菜单信息 + * + * @author ZhouXY + */ +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class MenuViewObject implements IWithOrderNumber { + + @Getter + @Setter + Integer type; + + @Getter + @Setter + String typeName; + + @Getter + @Setter + Long id; + @Getter + @Setter + Long parentId; + + @Getter + @Setter + String name; + // 若 type 为 MENU_ITEM 且 path 以 http:// 或 https:// 开头则被识别为外链 + @Getter + @Setter + String path; + @Getter + @Setter + String title; + @Getter + @Setter + String icon; + @Getter + @Setter + boolean hidden; + @Getter + @Setter + int orderNumber; + @Getter + @Setter + Integer status; + @Getter + @Setter + String remarks; + + // MENU_ITEM + @Getter + @Setter + String component; + @Getter + @Setter + Boolean cache; + @Getter + @Setter + String resource; + @Getter + @Setter + List actions; + + // MENU_LIST + List children; + + public void addChild(MenuViewObject child) { + if (this.children == null) { + this.children = new ArrayList<>(); + } + this.children.add(child); + } + + public void addChildren(Collection children) { + if (this.children == null) { + this.children = new ArrayList<>(); + } + this.children.addAll(children); + } + + public static MenuViewObject of(Menu menu) { + var viewObject = new MenuViewObject(); + viewObject.type = menu.getType().ordinal(); + viewObject.typeName = menu.getType().name(); + viewObject.id = menu.getId().orElseThrow(); + viewObject.parentId = menu.getParentId(); + viewObject.name = menu.getName(); + viewObject.path = menu.getPath(); + viewObject.title = menu.getTitle(); + viewObject.icon = menu.getIcon(); + viewObject.hidden = menu.isHidden(); + viewObject.orderNumber = menu.getOrderNumber(); + viewObject.status = menu.getStatus().getValue(); + viewObject.remarks = menu.getRemarks(); + if (viewObject.type == MenuType.MENU_ITEM.ordinal()) { + viewObject.component = menu.getComponent(); + viewObject.cache = menu.getCache(); + viewObject.resource = menu.getResource(); + viewObject.actions = menu.getActions(); + } + return viewObject; + } + + public List getChildren() { + return Objects.nonNull(this.children) + ? this.children + .stream() + .sorted(Comparator.comparing(IWithOrderNumber::getOrderNumber)) + .toList() + : null; + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/RoleOverview.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/RoleOverview.java new file mode 100644 index 0000000..b4ca3a5 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/query/result/RoleOverview.java @@ -0,0 +1,23 @@ +package xyz.zhouxy.plusone.system.application.query.result; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 角色信息 + * + * @author ZhouXY + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class RoleOverview { + // 角色 id + Long id; + // 角色名 + String name; + // 标识符(安全框架校验权限所用) + String identifier; + Integer status; +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/AccountContextService.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/AccountContextService.java new file mode 100644 index 0000000..e6e8996 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/AccountContextService.java @@ -0,0 +1,38 @@ +package xyz.zhouxy.plusone.system.application.service; + +import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import xyz.zhouxy.plusone.system.application.query.AccountQueries; +import xyz.zhouxy.plusone.system.application.query.result.AccountDetails; +import xyz.zhouxy.plusone.system.application.query.result.MenuViewObject; + +/** + * 账号查询本身相关信息 + * + * @author ZhouXY + */ +@Service +public class AccountContextService { + + private final AccountQueries accountQueries; + private final MenuManagementService menuManagementService; + + public AccountContextService(AccountQueries accountQueries, MenuManagementService menuManagementService) { + this.accountQueries = accountQueries; + this.menuManagementService = menuManagementService; + } + + public AccountDetails getAccountInfo() { + long accountId = adminAuthLogic.getLoginIdAsLong(); + return accountQueries.queryAccountDetails(accountId); + } + + public List getMenuTree() { + long accountId = adminAuthLogic.getLoginIdAsLong(); + return menuManagementService.queryByAccountId(accountId); + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/AccountManagementService.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/AccountManagementService.java new file mode 100644 index 0000000..e259d98 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/AccountManagementService.java @@ -0,0 +1,105 @@ +package xyz.zhouxy.plusone.system.application.service; + +import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic; + +import java.util.List; +import java.util.Objects; + +import javax.validation.Valid; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.PathVariable; + +import xyz.zhouxy.plusone.system.application.exception.AccountRegisterException; +import xyz.zhouxy.plusone.system.application.query.AccountQueries; +import xyz.zhouxy.plusone.system.application.query.params.AccountQueryParams; +import xyz.zhouxy.plusone.system.application.query.result.AccountDetails; +import xyz.zhouxy.plusone.system.application.query.result.AccountOverview; +import xyz.zhouxy.plusone.system.application.service.command.CreateAccountCommand; +import xyz.zhouxy.plusone.system.application.service.command.UpdateAccountCommand; +import xyz.zhouxy.plusone.system.domain.model.account.Account; +import xyz.zhouxy.plusone.system.domain.model.account.AccountInfo; +import xyz.zhouxy.plusone.system.domain.model.account.AccountRepository; +import xyz.zhouxy.plusone.system.domain.model.account.Email; +import xyz.zhouxy.plusone.system.domain.model.account.MobilePhone; +import xyz.zhouxy.plusone.system.domain.model.account.Username; +import xyz.zhouxy.plusone.util.AssertResult; +import xyz.zhouxy.plusone.util.PageDTO; + +/** + * 账号管理 + * + * @author ZhouXY + */ +@Service +@Transactional +public class AccountManagementService { + + private final AccountRepository accountRepository; + private final AccountQueries accountQueries; + + public AccountManagementService(AccountRepository accountRepository, AccountQueries accountQueries) { + this.accountRepository = accountRepository; + this.accountQueries = accountQueries; + } + + public void createAccount(@Valid CreateAccountCommand command) { + String username = command.getUsername(); + if (accountRepository.existsUsername(Username.of(username))) { + throw AccountRegisterException.usernameAlreadyExists(username); + } + String email = command.getEmail(); + if (StringUtils.hasText(email) && accountRepository.existsEmail(Email.of(email))) { + throw AccountRegisterException.emailAlreadyExists(email); + } + String mobilePhone = command.getMobilePhone(); + if (StringUtils.hasText(mobilePhone) && accountRepository.existsMobilePhone(MobilePhone.of(mobilePhone))) { + throw AccountRegisterException.mobilePhoneAlreadyExists(mobilePhone); + } + Account account = Account.newInstance( + username, + email, + mobilePhone, + command.getPassword(), + command.getPasswordConfirmation(), + command.getStatus(), + command.getRoleRefs(), + AccountInfo.of(command.getNickname(), command.getAvatar(), command.getSex()), + adminAuthLogic.getLoginIdAsLong()); + accountRepository.save(account); + } + + public void deleteAccounts(List ids) { + Account accountToDelete; + for (var id : ids) { + accountToDelete = accountRepository.find(id); + AssertResult.nonNull(accountToDelete); + accountRepository.delete(accountToDelete); + } + } + + public void updateAccountInfo(Long id, @Valid UpdateAccountCommand command) { + Assert.isTrue(Objects.equals(id, command.getId()), "参数错误: id 不匹配"); + Account account = accountRepository.find(id); + AssertResult.nonNull(account, "该账号不存在"); + account.setAccountInfo(command.getNickname(), command.getAvatar(), command.getSex()); + account.setUpdatedBy(adminAuthLogic.getLoginIdAsLong()); + accountRepository.save(account); + } + + @Transactional(propagation = Propagation.SUPPORTS) + public PageDTO queryAccountOverviewList(AccountQueryParams queryParams) { + return accountQueries.queryAccountOverviewPage(queryParams); + } + + @Transactional(propagation = Propagation.SUPPORTS) + public AccountDetails queryAccountDetails(@PathVariable("accountId") Long accountId) { + var accountDetails = accountQueries.queryAccountDetails(accountId); + AssertResult.nonNull(accountDetails); + return accountDetails; + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/AdminLoginService.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/AdminLoginService.java new file mode 100644 index 0000000..18cf975 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/AdminLoginService.java @@ -0,0 +1,104 @@ +package xyz.zhouxy.plusone.system.application.service; + +import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import xyz.zhouxy.plusone.exception.InvalidInputException; +import xyz.zhouxy.plusone.system.application.common.util.PrincipalType; +import xyz.zhouxy.plusone.system.application.common.util.PrincipalUtil; +import xyz.zhouxy.plusone.system.application.exception.AccountLoginException; +import xyz.zhouxy.plusone.system.application.query.AccountQueries; +import xyz.zhouxy.plusone.system.application.query.result.LoginInfoViewObject; +import xyz.zhouxy.plusone.system.application.service.command.LoginByOtpCommand; +import xyz.zhouxy.plusone.system.application.service.command.LoginByPasswordCommand; +import xyz.zhouxy.plusone.system.domain.model.account.Account; +import xyz.zhouxy.plusone.system.domain.model.account.AccountRepository; +import xyz.zhouxy.plusone.system.domain.model.account.Email; +import xyz.zhouxy.plusone.system.domain.model.account.MobilePhone; +import xyz.zhouxy.plusone.system.domain.model.account.Principal; +import xyz.zhouxy.plusone.system.domain.model.account.Username; +import xyz.zhouxy.plusone.validator.ValidateDto; + +/** + * Admin 账号登录 + * + * @author ZhouXY + */ +@Service +@Transactional +public class AdminLoginService { + + private final AccountRepository accountRepository; + private final AccountQueries accountQueries; + private final MailAndSmsVerifyService mailAndSmsVerifyService; + + AdminLoginService(AccountRepository accountRepository, AccountQueries accountQueries, + MailAndSmsVerifyService mailAndSmsVerifyService) { + this.accountRepository = accountRepository; + this.accountQueries = accountQueries; + this.mailAndSmsVerifyService = mailAndSmsVerifyService; + } + + @ValidateDto + public LoginInfoViewObject loginByPassword(LoginByPasswordCommand command) { + Principal principal = PrincipalUtil.getPrincipal(command.getPrincipal()); + Account account; + if (principal instanceof Email) { + account = accountRepository.findByEmail((Email) principal); + } else if (principal instanceof MobilePhone) { + account = accountRepository.findByMobilePhone((MobilePhone) principal); + } else { + account = accountRepository.findByUsername((Username) principal); + } + + if (account == null) { + throw AccountLoginException.accountNotExistException(); + } + @SuppressWarnings("null") + boolean isPasswordCorrect = account.checkPassword(command.getPassword()); + if (!isPasswordCorrect) { + throw AccountLoginException.passwordErrorException(); + } + adminAuthLogic.login(account.getId().orElseThrow(), command.isRememberMe()); + + var accountDetails = accountQueries.queryAccountDetails(account.getId().orElseThrow()); + return LoginInfoViewObject.of(adminAuthLogic.getTokenValue(), accountDetails); + } + + @ValidateDto + public LoginInfoViewObject loginByOtp(LoginByOtpCommand command) { + String principal = command.getPrincipal(); + PrincipalType principalType = PrincipalUtil.getPrincipalType(principal); + String otp = command.getOtp(); + boolean rememberMe = command.isRememberMe(); + + Account account; + if (principalType == PrincipalType.EMAIL) { + account = accountRepository.findByEmail(Email.of(principal)); + } else if (principalType == PrincipalType.MOBILE_PHONE) { + account = accountRepository.findByMobilePhone(MobilePhone.of(principal)); + } else { + throw InvalidInputException.unsupportedPrincipalTypeException("输入邮箱地址或手机号"); + } + + if (account == null) { + throw AccountLoginException.accountNotExistException(); + } + mailAndSmsVerifyService.checkOtp(principal, otp); + adminAuthLogic.login(account.getId().orElseThrow(), rememberMe); + + var accountDetails = accountQueries.queryAccountDetails(account.getId().orElseThrow()); + return LoginInfoViewObject.of(adminAuthLogic.getTokenValue(), accountDetails); + } + + public void sendOtp(String principal) { + Principal emailOrMobilePhone = PrincipalUtil.getEmailOrMobilePhone(principal); + if (emailOrMobilePhone instanceof Email) { + mailAndSmsVerifyService.sendOtpToEmail((Email) emailOrMobilePhone); + } else { + mailAndSmsVerifyService.sendOtpToMobilePhone((MobilePhone) emailOrMobilePhone); + } + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/AdminLogoutService.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/AdminLogoutService.java new file mode 100644 index 0000000..dd83505 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/AdminLogoutService.java @@ -0,0 +1,19 @@ +package xyz.zhouxy.plusone.system.application.service; + +import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic; + +import org.springframework.stereotype.Service; + +/** + * Admin 账号登出 + * + * @author ZhouXY + */ +@Service +public class AdminLogoutService { + + public void execute() { + adminAuthLogic.checkLogin(); + adminAuthLogic.logout(); + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/AuthService.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/AuthService.java new file mode 100644 index 0000000..2facf8f --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/AuthService.java @@ -0,0 +1,60 @@ +package xyz.zhouxy.plusone.system.application.service; + +import static xyz.zhouxy.plusone.system.constant.AuthLogic.*; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.stereotype.Service; + +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.stp.StpInterface; +import cn.dev33.satoken.stp.StpUtil; +import xyz.zhouxy.plusone.system.domain.model.menu.Action; +import xyz.zhouxy.plusone.system.domain.model.menu.MenuRepository; +import xyz.zhouxy.plusone.system.domain.model.role.ActionRef; +import xyz.zhouxy.plusone.system.domain.model.role.Role; +import xyz.zhouxy.plusone.system.domain.model.role.RoleRepository; + +@Service +public class AuthService implements StpInterface { + + private RoleRepository roleRepository; + private MenuRepository menuRepository; + + public AuthService(RoleRepository roleRepository, MenuRepository menuRepository) { + this.roleRepository = roleRepository; + this.menuRepository = menuRepository; + } + + @Override + public List getPermissionList(Object loginId, String loginType) { + Collection roles = getRoleList(loginId); + Set permissionIds = new HashSet<>(); + roles.forEach(role -> permissionIds.addAll( + role.getPermissions() + .stream() + .map(ActionRef::actionId) + .toList())); + List permValList = menuRepository.findPermissionsByIdIn(permissionIds) + .stream().map(Action::value).toList(); + return permValList; + } + + @Override + public List getRoleList(Object loginId, String loginType) { + SaSession session = switch (loginType) { + case ADMIN_LOGIN_TYPE -> adminAuthLogic.getSessionByLoginId(loginId); + case USER_LOGIN_TYPE -> userAuthLogic.getSessionByLoginId(loginId); + default -> StpUtil.getSessionByLoginId(loginId); + }; + return session.get("RoleList", + () -> getRoleList(loginId).stream().map(Role::getIdentifier).toList()); + } + + private Collection getRoleList(Object loginId) { + return roleRepository.findByAccountId(Long.valueOf((String) loginId)); + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/DictManagementService.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/DictManagementService.java new file mode 100644 index 0000000..28fba5b --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/DictManagementService.java @@ -0,0 +1,74 @@ +package xyz.zhouxy.plusone.system.application.service; + +import java.util.List; +import java.util.Objects; + +import javax.validation.Valid; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import xyz.zhouxy.plusone.system.application.query.DictQueries; +import xyz.zhouxy.plusone.system.application.query.params.DictQueryParams; +import xyz.zhouxy.plusone.system.application.query.result.DictOverview; +import xyz.zhouxy.plusone.system.application.service.command.CreateDictCommand; +import xyz.zhouxy.plusone.system.application.service.command.UpdateDictCommand; +import xyz.zhouxy.plusone.system.domain.model.dict.Dict; +import xyz.zhouxy.plusone.system.domain.model.dict.DictRepository; + +/** + * 数据字典管理 + * + * @author ZhouXY + */ +@Service +@Transactional +public class DictManagementService { + + private final DictRepository dictRepository; + private final DictQueries dictQueries; + + public DictManagementService(DictRepository dictRepository, DictQueries dictQueries) { + this.dictRepository = dictRepository; + this.dictQueries = dictQueries; + } + + public void createDict(@Valid CreateDictCommand command) { + var dictToSave = Dict.newInstance(command.getDictType(), + command.getDictLabel(), + command.getKeyLabelMap()); + dictRepository.save(dictToSave); + } + + public void deleteDicts(List ids) { + Dict dictToDelete; + for (Long id : ids) { + dictToDelete = dictRepository.find(id); + dictRepository.delete(dictToDelete); + } + } + + public void updateDict(Long id, @Valid UpdateDictCommand command) { + Assert.isTrue(Objects.equals(id, command.getId()), "id 不匹配"); + Dict dictToUpdate = dictRepository.find(command.getId()); + dictToUpdate.updateDict(command.getDictType(), command.getDictLabel(), command.getKeyLabelMap()); + dictRepository.save(dictToUpdate); + } + + @Transactional(propagation = Propagation.SUPPORTS) + public Dict findDictDetails(Long dictId) { + return dictRepository.find(dictId); + } + + @Transactional(propagation = Propagation.SUPPORTS) + public List loadAllDicts() { + return dictRepository.findAll(); + } + + @Transactional(propagation = Propagation.SUPPORTS) + public List queryDictOverviewList(@Valid DictQueryParams queryParams) { + return dictQueries.queryDictOverviewList(queryParams); + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/MailAndSmsVerifyService.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/MailAndSmsVerifyService.java new file mode 100644 index 0000000..0439fcb --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/MailAndSmsVerifyService.java @@ -0,0 +1,113 @@ +package xyz.zhouxy.plusone.system.application.service; + +import java.util.Objects; + +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import cn.hutool.core.util.RandomUtil; +import xyz.zhouxy.plusone.mail.MailService; +import xyz.zhouxy.plusone.sms.SmsService; +import xyz.zhouxy.plusone.system.application.exception.AccountLoginException; +import xyz.zhouxy.plusone.system.application.exception.AccountRegisterException; +import xyz.zhouxy.plusone.system.domain.model.account.AccountRepository; +import xyz.zhouxy.plusone.system.domain.model.account.Email; +import xyz.zhouxy.plusone.system.domain.model.account.MobilePhone; + +/** + * 邮箱和短信的验证服务 + */ +@Service +public class MailAndSmsVerifyService { + private static final int CODE_LENGTH = 6; + private final AccountRepository accountRepository; + private final MailService mailService; + private final SmsService smsService; + private final StringRedisTemplate redisTemplate; + + public MailAndSmsVerifyService(AccountRepository accountRepository, MailService mailService, SmsService smsService, + StringRedisTemplate redisTemplate) { + this.accountRepository = accountRepository; + this.mailService = mailService; + this.smsService = smsService; + this.redisTemplate = redisTemplate; + } + + /** + * 发送一次性密码到邮箱 + * + * @param email 要求邮箱必须已注册 + */ + public void sendOtpToEmail(Email email) { + Assert.isTrue(accountRepository.existsEmail(email), "该邮箱未绑定任何帐号"); + var otp = generateCode(); + mailService.sendCodeMail(otp, email.value()); + redisTemplate.opsForValue().set("OTP-" + email.value(), otp); + } + + /** + * 发送一次性密码到手机号 + * + * @param mobilePhone 要求手机号必须已注册 + */ + public void sendOtpToMobilePhone(MobilePhone mobilePhone) { + Assert.isTrue(accountRepository.existsMobilePhone(mobilePhone), "该手机号未绑定任何帐号"); + var otp = generateCode(); + smsService.sendCodeMessage(otp, mobilePhone.value()); + redisTemplate.opsForValue().set("OTP-" + mobilePhone.value(), otp); + } + + /** + * 发送校验码到邮箱 + * + * @param email 要求邮箱必须未注册 + */ + public void sendCodeToEmail(Email email) { + Assert.isTrue(!accountRepository.existsEmail(email), "该邮箱未绑定任何帐号"); + var code = generateCode(); + mailService.sendCodeMail(code, email.value()); + redisTemplate.opsForValue().set("Code-" + email.value(), code); + } + + /** + * 发送校验码到手机号 + * + * @param mobilePhone 要求手机号必须未注册 + */ + public void sendCodeToMobilePhone(MobilePhone mobilePhone) { + Assert.isTrue(!accountRepository.existsMobilePhone(mobilePhone), "该手机号未绑定任何帐号"); + var code = generateCode(); + smsService.sendCodeMessage(code, mobilePhone.value()); + redisTemplate.opsForValue().set("Code-" + mobilePhone.value(), code); + } + + public void checkOtp(String emailOrMobilePhone, String otp) { + String key = "OTP-" + emailOrMobilePhone; + String otpInRedis = redisTemplate.opsForValue().get(key); + if (otpInRedis == null) { + throw AccountLoginException.otpNotExistsException(); + } + if (!Objects.equals(otpInRedis, otp)) { + throw AccountLoginException.otpErrorException(); + } + redisTemplate.delete(key); + } + + public void checkCode(String emailOrMobilePhone, String code) { + String key = "Code-" + emailOrMobilePhone; + String codeInRedis = redisTemplate.opsForValue().get(key); + if (codeInRedis == null) { + throw AccountRegisterException.codeNotExistsException(); + } + if (!Objects.equals(codeInRedis, code)) { + throw AccountRegisterException.codeErrorException(); + } + redisTemplate.delete(key); + } + + private static String generateCode() { + return RandomUtil.randomString(CODE_LENGTH); + } + +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/MenuManagementService.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/MenuManagementService.java new file mode 100644 index 0000000..4f2a86e --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/MenuManagementService.java @@ -0,0 +1,174 @@ +package xyz.zhouxy.plusone.system.application.service; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import javax.validation.Valid; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import xyz.zhouxy.plusone.domain.IWithOrderNumber; +import xyz.zhouxy.plusone.system.application.exception.UnsupportedMenuTypeException; +import xyz.zhouxy.plusone.system.application.query.result.MenuViewObject; +import xyz.zhouxy.plusone.system.application.service.command.CreateMenuCommand; +import xyz.zhouxy.plusone.system.application.service.command.UpdateMenuCommand; +import xyz.zhouxy.plusone.system.domain.model.menu.Menu; +import xyz.zhouxy.plusone.system.domain.model.menu.MenuConstructor; +import xyz.zhouxy.plusone.system.domain.model.menu.MenuRepository; +import xyz.zhouxy.plusone.system.domain.service.MenuService; +import xyz.zhouxy.plusone.util.AssertResult; + +/** + * 菜单管理 + * + * @author ZhouXY + */ +@Service +@Transactional +public class MenuManagementService { + + private final MenuService menuService; + private final MenuRepository menuRepository; + + MenuManagementService(MenuService roleRepository, MenuRepository menuRepository) { + this.menuService = roleRepository; + this.menuRepository = menuRepository; + } + + // ==================== create ==================== + public void createMenu(@Valid CreateMenuCommand command) { + Menu menuToInsert; + switch (command.getMenuType()) { + case MENU_LIST: + menuToInsert = createMenuList(command); + break; + case MENU_ITEM: + menuToInsert = createMenuItem(command); + break; + default: + throw new UnsupportedMenuTypeException(); + } + menuRepository.save(menuToInsert); + } + + private Menu createMenuList(CreateMenuCommand command) { + return MenuConstructor.newMenuList( + command.getParentId(), + command.getPath(), + command.getName(), + command.getTitle(), + command.getIcon(), + command.getHidden(), + command.getOrderNumber(), + command.getStatus(), + command.getRemarks()); + } + + private Menu createMenuItem(CreateMenuCommand command) { + return MenuConstructor.newMenuItem( + command.getParentId(), + command.getPath(), + command.getName(), + command.getTitle(), + command.getIcon(), + command.getHidden(), + command.getOrderNumber(), + command.getStatus(), + command.getComponent(), + command.getResource(), + command.getCache(), + command.getRemarks()); + } + + // ==================== delete ==================== + public void deleteMenu(Long id) { + Menu menuToDelete = menuRepository.find(id); + AssertResult.nonNull(menuToDelete); + menuRepository.delete(menuToDelete); + } + + // ==================== update ==================== + public void updateMenu(Long id, @Valid UpdateMenuCommand command) { + Assert.isTrue(Objects.equals(id, command.getId()), "id 不匹配"); + Menu menuToUpdate = menuRepository.find(command.getId()); + menuToUpdate.updateMenuInfo( + command.getMenuType(), + command.getParentId(), + command.getPath(), + command.getName(), + command.getTitle(), + command.getIcon(), + command.getHidden(), + command.getOrderNumber(), + command.getStatus(), + command.getComponent(), + command.getResource(), + command.getCache(), + command.getRemarks()); + menuRepository.save(menuToUpdate); + } + + // ==================== query ==================== + @Transactional(propagation = Propagation.SUPPORTS) + public MenuViewObject findById(Long id) { + var menu = menuRepository.find(id); + return MenuViewObject.of(menu); + } + + @Transactional(propagation = Propagation.SUPPORTS) + public List queryByAccountId(Long accountId) { + var menus = menuService.queryAllMenuListByAccountId(accountId); + var menuViewObjects = menus.stream().map(MenuViewObject::of).toList(); + return buildMenuTree(menuViewObjects); + } + + @Transactional(propagation = Propagation.SUPPORTS) + public List queryByRoleId(Long roleId) { + var menus = menuRepository.queryByRoleId(roleId); + var menuViewObjects = menus.stream().map(MenuViewObject::of).toList(); + return buildMenuTree(menuViewObjects); + } + + @Transactional(propagation = Propagation.SUPPORTS) + public List buildMenuTree(List menus) { + List rootMenus = menus + .stream() + .filter(menu -> Objects.equals(menu.getParentId(), 0L)) + .toList(); + + Map allMenus = new HashMap<>(); + for (var item : menus) { + allMenus.put(item.getId(), item); + } + + for (MenuViewObject menu : menus) { + long parentId = menu.getParentId(); + while (parentId != 0 && !allMenus.containsKey(parentId)) { + MenuViewObject parent = findById(parentId); + if (parent == null) { + break; + } + allMenus.put(parent.getId(), parent); + parentId = parent.getParentId(); + } + } + + for (var menu : allMenus.values()) { + var parent = allMenus.getOrDefault(menu.getParentId(), null); + if (parent != null) { + parent.addChild(menu); + } + } + + return rootMenus + .stream() + .sorted(Comparator.comparing(IWithOrderNumber::getOrderNumber)) + .toList(); + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/RegisterAccountService.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/RegisterAccountService.java new file mode 100644 index 0000000..370a15f --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/RegisterAccountService.java @@ -0,0 +1,98 @@ +package xyz.zhouxy.plusone.system.application.service; + +import java.util.Set; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import xyz.zhouxy.plusone.exception.InvalidInputException; +import xyz.zhouxy.plusone.system.application.common.util.PrincipalType; +import xyz.zhouxy.plusone.system.application.common.util.PrincipalUtil; +import xyz.zhouxy.plusone.system.application.exception.AccountRegisterException; +import xyz.zhouxy.plusone.system.application.service.command.RegisterAccountCommand; +import xyz.zhouxy.plusone.system.domain.model.account.Account; +import xyz.zhouxy.plusone.system.domain.model.account.AccountInfo; +import xyz.zhouxy.plusone.system.domain.model.account.AccountRepository; +import xyz.zhouxy.plusone.system.domain.model.account.AccountStatus; +import xyz.zhouxy.plusone.system.domain.model.account.Email; +import xyz.zhouxy.plusone.system.domain.model.account.MobilePhone; +import xyz.zhouxy.plusone.system.domain.model.account.Password; +import xyz.zhouxy.plusone.system.domain.model.account.Username; + +/** + * 注册账号服务 + * + * @author ZhouXY + */ +@Service +@Transactional +public class RegisterAccountService { + + private static final long DEFAULT_ROLE_ID = 1L; + + private final AccountRepository accountRepository; + private final MailAndSmsVerifyService verifyService; + + public RegisterAccountService(AccountRepository accountRepository, MailAndSmsVerifyService verifyService) { + this.accountRepository = accountRepository; + this.verifyService = verifyService; + } + + public void registerAccount(RegisterAccountCommand command) { + String username = command.getUsername(); + var existsUsername = accountRepository.existsUsername(Username.of(username)); + if (existsUsername) { + throw AccountRegisterException.usernameAlreadyExists(username); + } + // 1. 确定是使用邮箱地址还是手机号进行注册 + String emailOrMobilePhone = command.getEmailOrMobilePhone(); + if (emailOrMobilePhone == null) { + throw new IllegalArgumentException("邮箱地址或手机号不能为空"); + } + PrincipalType principalType = PrincipalUtil.getPrincipalType(emailOrMobilePhone); + + // 2. 确定该邮箱地址或手机号是否存在,比对校验码 + Email email = null; + MobilePhone mobilePhone = null; + boolean isExists; + + if (principalType == PrincipalType.EMAIL) { + email = Email.of(emailOrMobilePhone); + isExists = accountRepository.existsEmail(email); + if (isExists) { + throw AccountRegisterException.emailAlreadyExists(email.value()); + } + } else if (principalType == PrincipalType.MOBILE_PHONE) { + mobilePhone = MobilePhone.of(emailOrMobilePhone); + isExists = accountRepository.existsMobilePhone(mobilePhone); + if (isExists) { + throw AccountRegisterException.emailAlreadyExists(mobilePhone.value()); + } + } else { + throw InvalidInputException.unsupportedPrincipalTypeException(); + } + + verifyService.checkCode(emailOrMobilePhone, command.getCode()); + + Account accountToSave = Account.register( + Username.of(username), + email, + mobilePhone, + Password.newPassword(command.getPassword(), command.getPasswordConfirmation()), + AccountStatus.AVAILABLE, + Set.of(DEFAULT_ROLE_ID), + AccountInfo.of(command.getNickname(), command.getAvatar(), command.getSex())); + accountRepository.save(accountToSave); + } + + public void sendCode(String principal) { + PrincipalType principalType = PrincipalUtil.getPrincipalType(principal); + if (principalType == PrincipalType.EMAIL) { + verifyService.sendCodeToEmail(Email.of(principal)); + } else if (principalType == PrincipalType.MOBILE_PHONE) { + verifyService.sendCodeToMobilePhone(MobilePhone.of(principal)); + } else { + throw InvalidInputException.unsupportedPrincipalTypeException(); + } + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/RoleManagementService.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/RoleManagementService.java new file mode 100644 index 0000000..23c68f7 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/RoleManagementService.java @@ -0,0 +1,95 @@ +package xyz.zhouxy.plusone.system.application.service; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.validation.Valid; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import xyz.zhouxy.plusone.system.application.query.RoleQueries; +import xyz.zhouxy.plusone.system.application.query.params.RoleQueryParams; +import xyz.zhouxy.plusone.system.application.query.result.RoleOverview; +import xyz.zhouxy.plusone.system.application.service.command.CreateRoleCommand; +import xyz.zhouxy.plusone.system.application.service.command.UpdateRoleCommand; +import xyz.zhouxy.plusone.system.domain.model.menu.MenuRepository; +import xyz.zhouxy.plusone.system.domain.model.role.ActionRef; +import xyz.zhouxy.plusone.system.domain.model.role.MenuRef; +import xyz.zhouxy.plusone.system.domain.model.role.Role; +import xyz.zhouxy.plusone.system.domain.model.role.RoleRepository; + +/** + * 角色管理服务 + * + * @author ZhouXY + */ +@Service +@Transactional +public class RoleManagementService { + + private final RoleRepository _roleRepository; + private final MenuRepository _menuRepository; + private final RoleQueries _roleQueries; + + public RoleManagementService(RoleRepository roleRepository, MenuRepository menuRepository, + RoleQueries roleQueries) { + _roleRepository = roleRepository; + _menuRepository = menuRepository; + _roleQueries = roleQueries; + } + + public void createRole(@Valid CreateRoleCommand command) { + Set menuRefs = _menuRepository.findByIdIn(command.getMenus()) + .stream() + .map(menu -> new MenuRef(menu.getId().orElseThrow())) + .collect(Collectors.toSet()); + Set permissionRefs = _menuRepository.findPermissionsByIdIn(command.getPermissions()) + .stream() + .map(permission -> new ActionRef(permission.getId().orElseThrow())) + .collect(Collectors.toSet()); + Role roleToCreate = Role.newInstance( + command.getName(), + command.getIdentifier(), + command.getStatus(), + command.getRemarks(), + menuRefs, + permissionRefs); + _roleRepository.save(roleToCreate); + } + + public void updateRole(@Valid UpdateRoleCommand command) { + Long roleId = command.getId(); + Role roleToUpdate = _roleRepository.find(roleId); + roleToUpdate.update( + command.getName(), + command.getIdentifier(), + command.getStatus(), + command.getRemarks(), + Set.copyOf(_menuRepository.findByIdIn(command.getMenus())), + Set.copyOf(_menuRepository.findPermissionsByIdIn(command.getPermissions()))); + _roleRepository.save(roleToUpdate); + } + + public void delete(Long id) { + Role role = _roleRepository.find(id); + _roleRepository.delete(role); + } + + @Transactional(propagation = Propagation.SUPPORTS) + public boolean exists(Long id) { + return _roleRepository.exists(id); + } + + @Transactional(propagation = Propagation.SUPPORTS) + public Role findById(Long id) { + return _roleRepository.find(id); + } + + @Transactional(propagation = Propagation.SUPPORTS) + public List query(RoleQueryParams params) { + return _roleQueries.query(params); + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/CreateAccountCommand.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/CreateAccountCommand.java new file mode 100644 index 0000000..c2b09d6 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/CreateAccountCommand.java @@ -0,0 +1,56 @@ +package xyz.zhouxy.plusone.system.application.service.command; + +import java.util.Set; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; + +import org.hibernate.validator.constraints.URL; + +import lombok.Data; +import xyz.zhouxy.plusone.constant.RegexConsts; +import xyz.zhouxy.plusone.domain.ICommand; +import xyz.zhouxy.plusone.system.domain.model.account.AccountStatus; +import xyz.zhouxy.plusone.system.domain.model.account.Sex; + +/** + * 创建账号命令 + * + * @author ZhouXY + */ +@Data +public class CreateAccountCommand implements ICommand { + + @NotBlank + @Pattern(regexp = RegexConsts.USERNAME, message = "用户名格式错误") + String username; + + @Email(message = "邮箱地址格式错误") + String email; + + @Pattern(regexp = RegexConsts.MOBILE_PHONE, message = "手机号格式错误") + String mobilePhone; + + @NotBlank + @Pattern(regexp = RegexConsts.PASSWORD, message = "密码不符合要求") + String password; + @NotBlank + @Pattern(regexp = RegexConsts.PASSWORD, message = "密码不符合要求") + String passwordConfirmation; + + @NotNull + AccountStatus status; + + Set roleRefs; + + @Pattern(regexp = RegexConsts.NICKNAME, message = "昵称格式错误") + String nickname; + + @NotBlank + @URL + String avatar; + + Sex sex; +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/CreateDictCommand.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/CreateDictCommand.java new file mode 100644 index 0000000..bd320a4 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/CreateDictCommand.java @@ -0,0 +1,27 @@ +package xyz.zhouxy.plusone.system.application.service.command; + +import java.util.Map; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +import lombok.Data; +import xyz.zhouxy.plusone.domain.ICommand; + +/** + * 创建数据字典命令 + * + * @author ZhouXY + */ +@Data +public class CreateDictCommand implements ICommand { + + @NotBlank + String dictType; + + @NotBlank + String dictLabel; + + @NotNull + Map keyLabelMap; +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/CreateMenuCommand.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/CreateMenuCommand.java new file mode 100644 index 0000000..d319517 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/CreateMenuCommand.java @@ -0,0 +1,55 @@ +package xyz.zhouxy.plusone.system.application.service.command; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +import lombok.Getter; +import lombok.Setter; +import xyz.zhouxy.plusone.constant.EntityStatus; +import xyz.zhouxy.plusone.domain.ICommand; +import xyz.zhouxy.plusone.system.domain.model.menu.Menu.MenuType; + +/** + * 创建菜单命令 + * + * @author ZhouXY + */ +@Getter +@Setter +public class CreateMenuCommand implements ICommand { + @NotNull + private MenuType menuType; + + @NotNull + private Long parentId; + + @NotBlank + private String path; + + @NotBlank + private String name; + + @NotBlank + private String title; + + @NotBlank + private String icon; + + @NotNull + private Boolean hidden; + + @NotNull + private Integer orderNumber; + + @NotNull + private EntityStatus status; + + private String component; + + private String resource; + + @NotNull + private Boolean cache; + + private String remarks; +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/CreateRoleCommand.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/CreateRoleCommand.java new file mode 100644 index 0000000..b0fd176 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/CreateRoleCommand.java @@ -0,0 +1,31 @@ +package xyz.zhouxy.plusone.system.application.service.command; + +import java.util.Set; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +import lombok.Data; +import xyz.zhouxy.plusone.constant.EntityStatus; +import xyz.zhouxy.plusone.domain.ICommand; + +/** + * 创建角色命令 + * + * @author ZhouXY + */ +@Data +public class CreateRoleCommand implements ICommand { + + @NotBlank + String name; + @NotBlank + String identifier; + + @NotNull + EntityStatus status; + String remarks; + + Set menus; + Set permissions; +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/LoginByOtpCommand.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/LoginByOtpCommand.java new file mode 100644 index 0000000..2bf1efc --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/LoginByOtpCommand.java @@ -0,0 +1,21 @@ +package xyz.zhouxy.plusone.system.application.service.command; + +import lombok.Data; +import xyz.zhouxy.plusone.domain.ICommand; + +/** + * 登录命令 + * + * @author ZhouXY + */ +@Data +public class LoginByOtpCommand implements ICommand { + + String principal; // 邮箱地址 / 手机号 + String otp; // 密码 + boolean rememberMe; // 记住我 + + // 进入登陆界面时或刷新验证码时,前端发送图形验证码的请求,后端生成 captcha 并暂存到 redis 中,key 为 UUID,将图形和 uuid 响应给前端。 + // String uuid; // 校验码的 key + // String captcha; // 校验码 +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/LoginByPasswordCommand.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/LoginByPasswordCommand.java new file mode 100644 index 0000000..518260a --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/LoginByPasswordCommand.java @@ -0,0 +1,21 @@ +package xyz.zhouxy.plusone.system.application.service.command; + +import lombok.Data; +import xyz.zhouxy.plusone.domain.ICommand; + +/** + * 登录命令 + * + * @author ZhouXY + */ +@Data +public class LoginByPasswordCommand implements ICommand { + + String principal; // 用户名 / 邮箱地址 / 手机号 + String password; // 密码 + boolean rememberMe; // 记住我 + + // 进入登陆界面时或刷新验证码时,前端发送图形验证码的请求,后端生成 captcha 并暂存到 redis 中,key 为 UUID,将图形和 uuid 响应给前端。 + // String uuid; // 校验码的 key + // String captcha; // 校验码 +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/RegisterAccountCommand.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/RegisterAccountCommand.java new file mode 100644 index 0000000..7d76d2b --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/RegisterAccountCommand.java @@ -0,0 +1,45 @@ +package xyz.zhouxy.plusone.system.application.service.command; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +import org.hibernate.validator.constraints.URL; + +import lombok.Data; +import xyz.zhouxy.plusone.constant.RegexConsts; +import xyz.zhouxy.plusone.domain.ICommand; +import xyz.zhouxy.plusone.system.domain.model.account.Sex; + +/** + * 注册账号命令 + * + * @author ZhouXY + */ +@Data +public class RegisterAccountCommand implements ICommand { + + @NotBlank + String emailOrMobilePhone; + + @NotBlank + @Pattern(regexp = RegexConsts.CAPTCHA) + String code; // 校验码 + + @NotBlank + @Pattern(regexp = RegexConsts.USERNAME) + String username; + + @NotBlank + @Pattern(regexp = RegexConsts.PASSWORD) + String password; + String passwordConfirmation; + + @Pattern(regexp = RegexConsts.NICKNAME) + String nickname; + + @NotBlank + @URL + String avatar; + + Sex sex; +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/UpdateAccountCommand.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/UpdateAccountCommand.java new file mode 100644 index 0000000..e45750c --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/UpdateAccountCommand.java @@ -0,0 +1,31 @@ +package xyz.zhouxy.plusone.system.application.service.command; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; + +import org.hibernate.validator.constraints.URL; + +import lombok.Data; +import xyz.zhouxy.plusone.constant.RegexConsts; +import xyz.zhouxy.plusone.domain.ICommand; +import xyz.zhouxy.plusone.system.domain.model.account.Sex; + +/** + * 更新账号信息命令 + * + * @author ZhouXY + */ +@Data +public class UpdateAccountCommand implements ICommand { + + @NotNull + Long id; + + @Pattern(regexp = RegexConsts.NICKNAME) + String nickname; + + @URL + String avatar; + + Sex sex; +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/UpdateDictCommand.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/UpdateDictCommand.java new file mode 100644 index 0000000..5813fb5 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/UpdateDictCommand.java @@ -0,0 +1,30 @@ +package xyz.zhouxy.plusone.system.application.service.command; + +import java.util.Map; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +import lombok.Data; +import xyz.zhouxy.plusone.domain.ICommand; + +/** + * 更新数据字典命令 + * + * @author ZhouXY + */ +@Data +public class UpdateDictCommand implements ICommand { + + @NotNull + Long id; + + @NotBlank + String dictType; + + @NotBlank + String dictLabel; + + @NotNull + Map keyLabelMap; +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/UpdateMenuCommand.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/UpdateMenuCommand.java new file mode 100644 index 0000000..2055d75 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/UpdateMenuCommand.java @@ -0,0 +1,59 @@ +package xyz.zhouxy.plusone.system.application.service.command; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +import lombok.Getter; +import lombok.Setter; +import xyz.zhouxy.plusone.constant.EntityStatus; +import xyz.zhouxy.plusone.domain.ICommand; +import xyz.zhouxy.plusone.system.domain.model.menu.Menu.MenuType; + +/** + * 更新菜单信息命令 + * + * @author ZhouXY + */ +@Getter +@Setter +public class UpdateMenuCommand implements ICommand { + + @NotNull + private Long id; + + @NotNull + private MenuType menuType; + + @NotNull + private Long parentId; + + @NotBlank + private String path; + + @NotBlank + private String name; + + @NotBlank + private String title; + + @NotBlank + private String icon; + + @NotNull + private Boolean hidden; + + @NotNull + private Integer orderNumber; + + @NotNull + private EntityStatus status; + + private String component; + + private String resource; + + @NotNull + private Boolean cache; + + private String remarks; +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/UpdateRoleCommand.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/UpdateRoleCommand.java new file mode 100644 index 0000000..1f267d0 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/UpdateRoleCommand.java @@ -0,0 +1,40 @@ +package xyz.zhouxy.plusone.system.application.service.command; + +import java.util.Set; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +import lombok.Data; +import xyz.zhouxy.plusone.constant.EntityStatus; +import xyz.zhouxy.plusone.domain.ICommand; + +/** + * 更新角色信息命令 + * + * @author ZhouXY + */ +@Data +public class UpdateRoleCommand implements ICommand { + + @NotNull + Long id; + + @NotBlank + String name; + + @NotBlank + String identifier; + + @NotNull + EntityStatus status; + + @NotBlank + String remarks; + + @NotNull + Set menus; + + @NotNull + Set permissions; +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/validator/LoginByOtpCommandValidator.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/validator/LoginByOtpCommandValidator.java new file mode 100644 index 0000000..b21b170 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/validator/LoginByOtpCommandValidator.java @@ -0,0 +1,29 @@ +package xyz.zhouxy.plusone.system.application.service.command.validator; + +import java.util.regex.Pattern; + +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import xyz.zhouxy.plusone.constant.RegexConsts; +import xyz.zhouxy.plusone.system.application.service.command.LoginByOtpCommand; +import xyz.zhouxy.plusone.util.RegexUtil; +import xyz.zhouxy.plusone.validator.BaseValidator; +import xyz.zhouxy.plusone.validator.DtoValidator; + +@Component +@DtoValidator(LoginByOtpCommand.class) +public class LoginByOtpCommandValidator extends BaseValidator { + public LoginByOtpCommandValidator() { + ruleFor(loginCommand -> { + String principal = loginCommand.getPrincipal(); + return StringUtils.hasText(principal) + && + RegexUtil.matchesOr(principal, RegexConsts.EMAIL, RegexConsts.MOBILE_PHONE); + }, "输入邮箱地址或手机号"); + ruleFor(loginCommand -> { + String otp = loginCommand.getOtp(); + return StringUtils.hasText(otp) && Pattern.matches(RegexConsts.CAPTCHA, otp); + }, "验证码不符合要求"); + } +} diff --git a/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/validator/LoginByPasswordCommandValidator.java b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/validator/LoginByPasswordCommandValidator.java new file mode 100644 index 0000000..3fa2e2a --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/java/xyz/zhouxy/plusone/system/application/service/command/validator/LoginByPasswordCommandValidator.java @@ -0,0 +1,31 @@ +package xyz.zhouxy.plusone.system.application.service.command.validator; + +import java.util.regex.Pattern; + +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import xyz.zhouxy.plusone.constant.RegexConsts; +import xyz.zhouxy.plusone.system.application.service.command.LoginByPasswordCommand; +import xyz.zhouxy.plusone.util.RegexUtil; +import xyz.zhouxy.plusone.validator.BaseValidator; +import xyz.zhouxy.plusone.validator.DtoValidator; + +@Component +@DtoValidator(LoginByPasswordCommand.class) +public class LoginByPasswordCommandValidator extends BaseValidator { + public LoginByPasswordCommandValidator() { + ruleFor(loginCommand -> { + String principal = loginCommand.getPrincipal(); + return StringUtils.hasText(principal) + && + RegexUtil.matchesOr(principal, RegexConsts.USERNAME, RegexConsts.EMAIL, RegexConsts.MOBILE_PHONE, principal); + }, "输入用户名、邮箱地址或手机号"); + ruleFor(loginCommand -> { + String password = loginCommand.getPassword(); + return StringUtils.hasText(password) + && + Pattern.matches(RegexConsts.PASSWORD, password); + }, "密码格式不正确"); + } +} diff --git a/plusone-system/plusone-system-application/src/main/resources/xyz/zhouxy/plusone/system/application/query/AccountQueries.xml b/plusone-system/plusone-system-application/src/main/resources/xyz/zhouxy/plusone/system/application/query/AccountQueries.xml new file mode 100644 index 0000000..20bf6b4 --- /dev/null +++ b/plusone-system/plusone-system-application/src/main/resources/xyz/zhouxy/plusone/system/application/query/AccountQueries.xml @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plusone-system/plusone-system-common/pom.xml b/plusone-system/plusone-system-common/pom.xml new file mode 100644 index 0000000..e95bfee --- /dev/null +++ b/plusone-system/plusone-system-common/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + plusone-system + xyz.zhouxy + 1.0.0-SNAPSHOT + + + plusone-system-common + + + + xyz.zhouxy + plusone-basic-common + + + cn.hutool + hutool-crypto + + + + diff --git a/plusone-system/plusone-system-common/src/main/java/xyz/zhouxy/plusone/system/constant/AuthLogic.java b/plusone-system/plusone-system-common/src/main/java/xyz/zhouxy/plusone/system/constant/AuthLogic.java new file mode 100644 index 0000000..7b8f728 --- /dev/null +++ b/plusone-system/plusone-system-common/src/main/java/xyz/zhouxy/plusone/system/constant/AuthLogic.java @@ -0,0 +1,20 @@ +package xyz.zhouxy.plusone.system.constant; + +import cn.dev33.satoken.stp.StpLogic; + +/** + * 维护 StpLogic 的实例 + * + * @author ZhouXY + */ +public class AuthLogic { + private AuthLogic() { + throw new IllegalStateException("Utility class"); + } + + public static final String ADMIN_LOGIN_TYPE = "Admin"; + public static final StpLogic adminAuthLogic = new StpLogic(ADMIN_LOGIN_TYPE); + + public static final String USER_LOGIN_TYPE = "User"; + public static final StpLogic userAuthLogic = new StpLogic(USER_LOGIN_TYPE); +} diff --git a/plusone-system/plusone-system-common/src/main/java/xyz/zhouxy/plusone/system/util/PasswordUtil.java b/plusone-system/plusone-system-common/src/main/java/xyz/zhouxy/plusone/system/util/PasswordUtil.java new file mode 100644 index 0000000..c0ac1bb --- /dev/null +++ b/plusone-system/plusone-system-common/src/main/java/xyz/zhouxy/plusone/system/util/PasswordUtil.java @@ -0,0 +1,51 @@ +package xyz.zhouxy.plusone.system.util; + +import javax.annotation.Nonnull; + +import cn.hutool.core.util.RandomUtil; +import cn.hutool.crypto.digest.DigestUtil; +import xyz.zhouxy.plusone.exception.PlusoneException; + +/** + * 密码工具类 + * + * @author ZhouXY + */ +public final class PasswordUtil { + private static final String SALT_BASE_STRING = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~`!@#$%^&*()_-+={}[]|\\:;\"',.<>?/"; + + /** + * 将密码和随机盐混合,并进行哈希加密。 + * + * @param password 密文密码 + * @param salt 随机盐 + * @return 哈希加密的结果 + */ + @Nonnull + public static String hashPassword(@Nonnull String password, @Nonnull String salt) { + int length = salt.length(); + int i = length > 0 ? length / 2 : 0; + var passwordWithSalt = salt.substring(0, i) + + password + + salt.substring(1); + String sha512Hex = DigestUtil.sha512Hex(passwordWithSalt); + if (sha512Hex == null) { + throw new PlusoneException(9999999, "未知错误:哈希加密失败!"); + } + return sha512Hex; + } + + /** + * 生成 24 位的字符串 + * + * @return 生成的随机盐 + */ + public static String generateRandomSalt() { + return RandomUtil.randomString(SALT_BASE_STRING, 24); + } + + private PasswordUtil() { + // 不允许实例化 + throw new IllegalStateException("Utility class"); + } +} diff --git a/plusone-system/plusone-system-domain/pom.xml b/plusone-system/plusone-system-domain/pom.xml new file mode 100644 index 0000000..f2b0c39 --- /dev/null +++ b/plusone-system/plusone-system-domain/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + xyz.zhouxy + plusone-system + 1.0.0-SNAPSHOT + + + plusone-system-domain + + + + xyz.zhouxy + plusone-basic-domain + + + xyz.zhouxy + plusone-system-common + + + + diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/AccountCreated.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/AccountCreated.java new file mode 100644 index 0000000..4b9621c --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/AccountCreated.java @@ -0,0 +1,33 @@ +package xyz.zhouxy.plusone.system.domain.event; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import xyz.zhouxy.plusone.domain.DomainEvent; +import xyz.zhouxy.plusone.system.domain.model.account.Account; +import xyz.zhouxy.plusone.system.domain.model.account.Email; +import xyz.zhouxy.plusone.system.domain.model.account.MobilePhone; +import xyz.zhouxy.plusone.system.domain.model.account.Username; + +/** + * 领域事件:创建账号事件 + * + * @author ZhouXY + */ +@Getter +@Setter +@NoArgsConstructor +@ToString(callSuper = true) +public class AccountCreated extends DomainEvent { + + private Username username; + private Email email; + private MobilePhone mobilePhone; + + public AccountCreated(Account account) { + this.username = account.getUsername(); + this.email = account.getEmail(); + this.mobilePhone = account.getMobilePhone(); + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/AccountLocked.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/AccountLocked.java new file mode 100644 index 0000000..3bdb683 --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/AccountLocked.java @@ -0,0 +1,33 @@ +package xyz.zhouxy.plusone.system.domain.event; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import xyz.zhouxy.plusone.domain.DomainEvent; +import xyz.zhouxy.plusone.system.domain.model.account.Account; +import xyz.zhouxy.plusone.system.domain.model.account.Email; +import xyz.zhouxy.plusone.system.domain.model.account.MobilePhone; +import xyz.zhouxy.plusone.system.domain.model.account.Username; + +/** + * 领域事件:账号被锁定事件 + * + * @author ZhouXY + */ +@Getter +@Setter +@NoArgsConstructor +@ToString(callSuper = true) +public class AccountLocked extends DomainEvent { + + private Username username; + private Email email; + private MobilePhone mobilePhone; + + public AccountLocked(Account account) { + this.username = account.getUsername(); + this.email = account.getEmail(); + this.mobilePhone = account.getMobilePhone(); + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/AccountPasswordChanged.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/AccountPasswordChanged.java new file mode 100644 index 0000000..5e464ad --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/AccountPasswordChanged.java @@ -0,0 +1,34 @@ +package xyz.zhouxy.plusone.system.domain.event; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import xyz.zhouxy.plusone.domain.DomainEvent; +import xyz.zhouxy.plusone.system.domain.model.account.Account; +import xyz.zhouxy.plusone.system.domain.model.account.Email; +import xyz.zhouxy.plusone.system.domain.model.account.MobilePhone; +import xyz.zhouxy.plusone.system.domain.model.account.Username; + +/** + * 领域事件:账号密码被更改事件 + * + * @author ZhouXY + */ +@Getter +@Setter +@NoArgsConstructor +@ToString(callSuper = true) +public class AccountPasswordChanged extends DomainEvent { + + private Username username; + private Email email; + private MobilePhone mobilePhone; + + public AccountPasswordChanged(Account account) { + this.username = account.getUsername(); + this.email = account.getEmail(); + this.mobilePhone = account.getMobilePhone(); + } + +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/AccountRecovered.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/AccountRecovered.java new file mode 100644 index 0000000..8bd4bd7 --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/AccountRecovered.java @@ -0,0 +1,27 @@ +package xyz.zhouxy.plusone.system.domain.event; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import xyz.zhouxy.plusone.domain.DomainEvent; +import xyz.zhouxy.plusone.system.domain.model.account.Account; +import xyz.zhouxy.plusone.system.domain.model.account.Username; + +/** + * 领域事件:账号恢复事件 + * + * @author ZhouXY + */ +@Getter +@Setter +@NoArgsConstructor +@ToString(callSuper = true) +public class AccountRecovered extends DomainEvent { + + private Username username; + + public AccountRecovered(Account account) { + this.username = account.getUsername(); + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/AccountRolesBound.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/AccountRolesBound.java new file mode 100644 index 0000000..d2e9848 --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/AccountRolesBound.java @@ -0,0 +1,27 @@ +package xyz.zhouxy.plusone.system.domain.event; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import xyz.zhouxy.plusone.domain.DomainEvent; +import xyz.zhouxy.plusone.system.domain.model.account.Account; +import xyz.zhouxy.plusone.system.domain.model.account.Username; + +/** + * 领域事件:账号绑定角色事件 + * + * @author ZhouXY + */ +@Getter +@Setter +@NoArgsConstructor +@ToString(callSuper = true) +public class AccountRolesBound extends DomainEvent { + + private Username username; + + public AccountRolesBound(Account account) { + this.username = account.getUsername(); + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/EmailChanged.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/EmailChanged.java new file mode 100644 index 0000000..34bd0cf --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/EmailChanged.java @@ -0,0 +1,30 @@ +package xyz.zhouxy.plusone.system.domain.event; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import xyz.zhouxy.plusone.domain.DomainEvent; +import xyz.zhouxy.plusone.system.domain.model.account.Account; +import xyz.zhouxy.plusone.system.domain.model.account.Email; +import xyz.zhouxy.plusone.system.domain.model.account.Username; + +/** + * 领域事件:账号邮箱更改事件 + * + * @author ZhouXY + */ +@Getter +@Setter +@NoArgsConstructor +@ToString(callSuper = true) +public class EmailChanged extends DomainEvent { + + private Username username; + private Email email; + + public EmailChanged(Account account) { + this.username = account.getUsername(); + this.email = account.getEmail(); + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/MobilePhoneChanged.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/MobilePhoneChanged.java new file mode 100644 index 0000000..d982efc --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/MobilePhoneChanged.java @@ -0,0 +1,30 @@ +package xyz.zhouxy.plusone.system.domain.event; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import xyz.zhouxy.plusone.domain.DomainEvent; +import xyz.zhouxy.plusone.system.domain.model.account.Account; +import xyz.zhouxy.plusone.system.domain.model.account.MobilePhone; +import xyz.zhouxy.plusone.system.domain.model.account.Username; + +/** + * 领域事件:账号手机号更改事件 + * + * @author ZhouXY + */ +@Getter +@Setter +@NoArgsConstructor +@ToString(callSuper = true) +public class MobilePhoneChanged extends DomainEvent { + + private Username username; + private @Getter @Setter MobilePhone mobilePhone; + + public MobilePhoneChanged(Account account) { + this.username = account.getUsername(); + this.mobilePhone = account.getMobilePhone(); + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/UsernameChanged.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/UsernameChanged.java new file mode 100644 index 0000000..95db6a7 --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/event/UsernameChanged.java @@ -0,0 +1,33 @@ +package xyz.zhouxy.plusone.system.domain.event; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import xyz.zhouxy.plusone.domain.DomainEvent; +import xyz.zhouxy.plusone.system.domain.model.account.Account; +import xyz.zhouxy.plusone.system.domain.model.account.Email; +import xyz.zhouxy.plusone.system.domain.model.account.MobilePhone; +import xyz.zhouxy.plusone.system.domain.model.account.Username; + +/** + * 领域事件:账号用户名修改事件 + * + * @author ZhouXY + */ +@Getter +@Setter +@NoArgsConstructor +@ToString(callSuper = true) +public class UsernameChanged extends DomainEvent { + + private Username username; + private Email email; + private MobilePhone mobilePhone; + + public UsernameChanged(Account account) { + this.username = account.getUsername(); + this.email = account.getEmail(); + this.mobilePhone = account.getMobilePhone(); + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Account.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Account.java new file mode 100644 index 0000000..e19ab3a --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Account.java @@ -0,0 +1,235 @@ +package xyz.zhouxy.plusone.system.domain.model.account; + +import java.net.URL; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +import javax.annotation.Nonnull; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import xyz.zhouxy.plusone.domain.AggregateRoot; +import xyz.zhouxy.plusone.domain.IWithVersion; +import xyz.zhouxy.plusone.exception.UserOperationException; +import xyz.zhouxy.plusone.system.domain.event.AccountCreated; +import xyz.zhouxy.plusone.system.domain.event.AccountLocked; +import xyz.zhouxy.plusone.system.domain.event.AccountPasswordChanged; +import xyz.zhouxy.plusone.system.domain.event.AccountRecovered; +import xyz.zhouxy.plusone.system.domain.event.AccountRolesBound; +import xyz.zhouxy.plusone.system.domain.event.EmailChanged; +import xyz.zhouxy.plusone.system.domain.event.MobilePhoneChanged; +import xyz.zhouxy.plusone.system.domain.event.UsernameChanged; + +/** + * 聚合根:账号 + * + * @author ZhouXY + */ +@ToString +public class Account extends AggregateRoot implements IWithVersion { + + // ===================== 字段 ==================== + private Long id; + private @Getter Username username; + private @Getter Email email; + private @Getter MobilePhone mobilePhone; + private Password password; + private @Getter AccountStatus status; + private @Getter AccountInfo accountInfo; + private Set roleRefs = new HashSet<>(); + + private @Getter Long createdBy; + private @Getter @Setter Long updatedBy; + private @Getter long version; + + public void setUsername(Username username) { + this.username = username; + addDomainEvent(new UsernameChanged(this)); + } + + public void setEmail(Email email) { + this.email = email; + addDomainEvent(new EmailChanged(this)); + } + + public void setMobilePhone(MobilePhone mobilePhone) { + this.mobilePhone = mobilePhone; + addDomainEvent(new MobilePhoneChanged(this)); + } + + public void changePassword(String newPassword, String passwordConfirmation) { + this.password = Password.newPassword(newPassword, passwordConfirmation); + addDomainEvent(new AccountPasswordChanged(this)); + } + + /** + * 锁定账号。如当前账号已锁定,则抛出 UserOperationException 异常 + * + * @see UserOperationException + */ + public void lockAccount() { + if (this.status == AccountStatus.LOCKED) { + throw UserOperationException + .invalidOperation(String.format("账号 %d 的状态为:%s,无法锁定", this.id, this.status.getName())); + } + this.status = AccountStatus.LOCKED; + addDomainEvent(new AccountLocked(this)); + } + + /** + * 恢复账号状态。如当前用户可用,则抛出 UserOperationException 异常 + * + * @see UserOperationException + */ + public void recoveryAccount() { + if (this.status == AccountStatus.AVAILABLE) { + throw UserOperationException + .invalidOperation(String.format("账号 %d 的状态为:%s,无法恢复", this.id, this.status.getName())); + } + this.status = AccountStatus.AVAILABLE; + addDomainEvent(new AccountRecovered(this)); + } + + public void setAccountInfo(AccountInfo accountInfo) { + this.accountInfo = accountInfo; + } + + public void setAccountInfo(Nickname nickname, URL avatar, Sex sex) { + this.accountInfo = AccountInfo.of(nickname, avatar, sex); + } + + public void setAccountInfo(String nickname, String avatar, Sex sex) { + this.accountInfo = AccountInfo.of(nickname, avatar, sex); + } + + /** + * 绑定角色 + * + * @param roleRefs 角色 id 集合 + */ + public void bindRoles(Set roleRefs) { + this.roleRefs.clear(); + this.roleRefs.addAll(roleRefs); + addDomainEvent(new AccountRolesBound(this)); + } + + public boolean checkPassword(@Nonnull String password) { + return this.password.check(password); + } + + // ===================== 实例化 ==================== + + Account(Long id, + Username username, + Email email, + MobilePhone mobilePhone, + Password password, + AccountStatus status, + AccountInfo accountInfo, + Set roleRefs, + Long createdBy, + Long updatedBy, + long version) { + this.id = id; + this.username = username; + this.email = email; + this.mobilePhone = mobilePhone; + this.password = password; + this.status = status; + this.accountInfo = accountInfo; + this.bindRoles(roleRefs); + this.createdBy = createdBy; + this.updatedBy = updatedBy; + this.version = version; + } + + public static Account newInstance( + Username username, + Email email, + MobilePhone mobilePhone, + Password password, + AccountStatus status, + Set roleRefs, + AccountInfo accountInfo, + Long createdBy) { + var newInstance = new Account(null, username, email, mobilePhone, password, status, accountInfo, roleRefs, + createdBy, null, 0); + newInstance.addDomainEvent(new AccountCreated(newInstance)); + return newInstance; + } + + public static Account register( + Username username, + Email email, + MobilePhone mobilePhone, + Password password, + AccountStatus status, + Set roleRefs, + AccountInfo accountInfo) { + var newInstance = new Account(null, username, email, mobilePhone, password, status, accountInfo, roleRefs, + 0L, null, 0); + newInstance.addDomainEvent(new AccountCreated(newInstance)); + return newInstance; + } + + Account(Long id, + String username, + String email, + String mobilePhone, + Password password, + AccountStatus status, + AccountInfo accountInfo, + Set roleRefs, + Long createdBy, + Long updatedBy, + long version) { + this(id, Username.of(username), Email.ofNullable(email), MobilePhone.ofNullable(mobilePhone), + password, status, accountInfo, roleRefs, createdBy, updatedBy, version); + } + + public static Account newInstance( + String username, + String email, + String mobilePhone, + String password, + String passwordConfirmation, + AccountStatus status, + Set roleRefs, + AccountInfo accountInfo, + long createdBy) { + var newInstance = new Account(null, username, email, mobilePhone, + Password.newPassword(password, passwordConfirmation), status, accountInfo, roleRefs, + createdBy, null, 0); + newInstance.addDomainEvent(new AccountCreated(newInstance)); + return newInstance; + } + + public static Account register( + String username, + String email, + String mobilePhone, + Password password, + AccountStatus status, + Set roleRefs, + AccountInfo accountInfo) { + var newInstance = new Account(null, username, email, mobilePhone, password, status, accountInfo, roleRefs, + 0L, null, 0); + newInstance.addDomainEvent(new AccountCreated(newInstance)); + return newInstance; + } + + @Override + public Optional getId() { + return Optional.ofNullable(id); + } + + public Set getRoleIds() { + return Set.copyOf(this.roleRefs); + } + + Password getPassword() { + return password; + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/AccountInfo.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/AccountInfo.java new file mode 100644 index 0000000..3fcafbb --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/AccountInfo.java @@ -0,0 +1,43 @@ +package xyz.zhouxy.plusone.system.domain.model.account; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Objects; + +import lombok.Getter; +import lombok.ToString; +import xyz.zhouxy.plusone.domain.IValueObject; + +/** + * 账号详细信息 + * + * @author ZhouXY + */ +@Getter +@ToString +public class AccountInfo implements IValueObject { + + private final Nickname nickname; + private final URL avatar; + private final Sex sex; + + private AccountInfo(Nickname nickname, URL avatar, Sex sex) { + this.nickname = nickname; + this.avatar = avatar; + this.sex = Objects.nonNull(sex) ? sex : Sex.UNSET; + } + + public static AccountInfo of(Nickname nickname, URL avatar, Sex sex) { + return new AccountInfo(nickname, avatar, sex); + } + + public static AccountInfo of(String nickname, String avatar, Sex sex) { + URL avatarURL; + try { + avatarURL = Objects.nonNull(avatar) ? new URL(avatar) : null; + } catch (MalformedURLException e) { + throw new IllegalArgumentException(e); + } + return new AccountInfo(Nickname.ofNullable(nickname), avatarURL, Objects.nonNull(sex) ? sex : Sex.UNSET); + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/AccountRepository.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/AccountRepository.java new file mode 100644 index 0000000..3526f84 --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/AccountRepository.java @@ -0,0 +1,29 @@ +package xyz.zhouxy.plusone.system.domain.model.account; + +import java.util.Collection; + +import xyz.zhouxy.plusone.domain.IRepository; + +/** + * AccountRepository + * + * @author ZhouXY + * @see Account + */ +public interface AccountRepository extends IRepository { + + Collection findByRoleId(Long roleId); + + Account findByEmail(Email email); + + Account findByMobilePhone(MobilePhone mobilePhone); + + Account findByUsername(Username username); + + boolean existsUsername(Username username); + + boolean existsEmail(Email email); + + boolean existsMobilePhone(MobilePhone email); + +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/AccountStatus.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/AccountStatus.java new file mode 100644 index 0000000..1ecaef3 --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/AccountStatus.java @@ -0,0 +1,29 @@ +package xyz.zhouxy.plusone.system.domain.model.account; + +import lombok.Getter; +import xyz.zhouxy.plusone.domain.IValueObject; +import xyz.zhouxy.plusone.util.Enumeration; +import xyz.zhouxy.plusone.util.EnumerationValuesHolder; + +/** + * 账号状态 + * + * @author ZhouXY + */ +@Getter +public class AccountStatus extends Enumeration implements IValueObject { + + private AccountStatus(int value, String name) { + super(value, name); + } + + public static final AccountStatus AVAILABLE = new AccountStatus(0, "账号正常"); + public static final AccountStatus LOCKED = new AccountStatus(1, "账号被锁定"); + + private static final EnumerationValuesHolder ENUMERATION_VALUES = new EnumerationValuesHolder<>( + new AccountStatus[] { AVAILABLE, LOCKED }); + + public static AccountStatus of(int value) { + return ENUMERATION_VALUES.get(value); + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Email.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Email.java new file mode 100644 index 0000000..f09d3b7 --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Email.java @@ -0,0 +1,47 @@ +package xyz.zhouxy.plusone.system.domain.model.account; + +import java.util.Objects; + +import cn.hutool.core.util.DesensitizedUtil; +import xyz.zhouxy.plusone.constant.RegexConsts; + +/** + * 值对象:电子邮箱地址 + * + * @author ZhouXY + */ +public class Email extends Principal { + + public static final String REGEX = RegexConsts.EMAIL; + + private Email(String email) { + super(REGEX); + if (email == null) { + throw new IllegalArgumentException("邮箱地址不能为空"); + } + this.value = email; + if (!isValid()) { + throw new IllegalArgumentException("邮箱地址格式错误"); + } + } + + public static Email of(String email) { + return new Email(email); + } + + public static Email ofNullable(String email) { + return Objects.nonNull(email) ? new Email(email) : null; + } + + /** + * 脱敏后的数据 + */ + public String safeValue() { + return DesensitizedUtil.email(this.value); + } + + @Override + public String toString() { + return this.safeValue(); + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/MobilePhone.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/MobilePhone.java new file mode 100644 index 0000000..ebf5b2e --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/MobilePhone.java @@ -0,0 +1,44 @@ +package xyz.zhouxy.plusone.system.domain.model.account; + +import java.util.Objects; + +import cn.hutool.core.util.DesensitizedUtil; +import xyz.zhouxy.plusone.constant.RegexConsts; + +/** + * 值对象:手机号码 + * + * @author ZhouXY + */ +public class MobilePhone extends Principal { + + public static final String REGEX = RegexConsts.MOBILE_PHONE; + + private MobilePhone(String mobilePhone) { + super(REGEX); + if (mobilePhone == null) { + throw new IllegalArgumentException("手机号不能为空"); + } + this.value = mobilePhone; + if (!isValid()) { + throw new IllegalArgumentException("手机号格式错误"); + } + } + + public static MobilePhone of(String mobilePhone) { + return new MobilePhone(mobilePhone); + } + + public static MobilePhone ofNullable(String mobilePhone) { + return Objects.nonNull(mobilePhone) ? new MobilePhone(mobilePhone) : null; + } + + public String safeValue() { + return DesensitizedUtil.mobilePhone(this.value); + } + + @Override + public String toString() { + return this.safeValue(); + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Nickname.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Nickname.java new file mode 100644 index 0000000..7fcd33a --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Nickname.java @@ -0,0 +1,35 @@ +package xyz.zhouxy.plusone.system.domain.model.account; + +import java.util.Objects; + +import xyz.zhouxy.plusone.constant.RegexConsts; +import xyz.zhouxy.plusone.domain.ValidatableStringRecord; + +/** + * 值对象:昵称 + * + * @author ZhouXY + */ +public class Nickname extends ValidatableStringRecord { + + public static final String REGEX = RegexConsts.NICKNAME; + + private Nickname(String value) { + super(REGEX); + if (value == null) { + throw new IllegalArgumentException("昵称不能为空"); + } + this.value = value; + if (!isValid()) { + throw new IllegalArgumentException("昵称格式错误"); + } + } + + public static Nickname of(String nickname) { + return new Nickname(nickname); + } + + public static Nickname ofNullable(String nickname) { + return Objects.nonNull(nickname) ? new Nickname(nickname) : null; + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Password.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Password.java new file mode 100644 index 0000000..5388195 --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Password.java @@ -0,0 +1,90 @@ +package xyz.zhouxy.plusone.system.domain.model.account; + +import java.util.Objects; +import java.util.regex.Pattern; + +import javax.annotation.Nonnull; + +import org.springframework.util.Assert; + +import xyz.zhouxy.plusone.constant.RegexConsts; +import xyz.zhouxy.plusone.domain.IValueObject; +import xyz.zhouxy.plusone.exception.PlusoneException; +import xyz.zhouxy.plusone.system.util.PasswordUtil; + +/** + * 值对象:加盐加密的密码 + * + * @author ZhouXY + */ +public class Password implements IValueObject { + + private static final Pattern PATTERN = Pattern.compile(RegexConsts.PASSWORD); + private static final String DEFAULT_PASSWORD = "A1b2C3d4"; + + @Nonnull + private final String passwordVal; + @Nonnull + private final String saltVal; + + private Password(String password) { + if (password == null) { + throw new IllegalArgumentException("密码不能为空"); + } + if (!PATTERN.matcher(password).matches()) { + throw new IllegalArgumentException("密码格式不符合要求"); + } + var salt = PasswordUtil.generateRandomSalt(); + if (salt == null) { + throw new PlusoneException(9999999, "未知错误:生成随机盐失败"); + } + this.saltVal = salt; + this.passwordVal = PasswordUtil.hashPassword(password, salt); + } + + private Password(String password, String salt) { + if (password == null || salt == null) { + throw new IllegalArgumentException("password 和 salt 不能为空"); + } + this.passwordVal = password; + this.saltVal = salt; + } + + public static Password of(String password, String salt) { + return new Password(password, salt); + } + + public static Password newPassword(String newPassword, String passwordConfirmation) { + Assert.isTrue(Objects.equals(newPassword, passwordConfirmation), "两次输入的密码不一致"); + return newPassword(newPassword); + } + + public static Password newPassword(String newPassword) { + return new Password(newPassword); + } + + public boolean check(String password) { + if (password == null) { + throw new IllegalArgumentException("password 不能为空"); + } + Assert.hasText(password, "密码不能为空"); + return Objects.equals(this.passwordVal, PasswordUtil.hashPassword(password, this.saltVal)); + } + + public String value() { + return passwordVal; + } + + public String getSalt() { + return saltVal; + } + + public static Password newDefaultPassword() { + return newPassword(DEFAULT_PASSWORD); + } + + @Override + public String toString() { + return "********"; + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Principal.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Principal.java new file mode 100644 index 0000000..b622630 --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Principal.java @@ -0,0 +1,14 @@ +package xyz.zhouxy.plusone.system.domain.model.account; + +import xyz.zhouxy.plusone.domain.ValidatableStringRecord; + +/** + * 账号标识符 + * + * @author ZhouXY + */ +public abstract class Principal extends ValidatableStringRecord { + protected Principal(String format) { + super(format); + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Sex.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Sex.java new file mode 100644 index 0000000..cb7112b --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Sex.java @@ -0,0 +1,30 @@ +package xyz.zhouxy.plusone.system.domain.model.account; + +import xyz.zhouxy.plusone.domain.IValueObject; +import xyz.zhouxy.plusone.util.Enumeration; +import xyz.zhouxy.plusone.util.EnumerationValuesHolder; + +/** + * 值对象:性别 + * + * @author ZhouXY + */ +public class Sex extends Enumeration implements IValueObject { + public static final Sex UNSET = new Sex(0, "未设置"); + public static final Sex MALE = new Sex(1, "男性"); + public static final Sex FEMALE = new Sex(2, "女性"); + + private Sex(int value, String name) { + super(value, name); + } + + private static EnumerationValuesHolder values = new EnumerationValuesHolder<>(new Sex[] { + UNSET, + MALE, + FEMALE + }); + + public static Sex of(int value) { + return values.get(value); + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Username.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Username.java new file mode 100644 index 0000000..94eab93 --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/Username.java @@ -0,0 +1,28 @@ +package xyz.zhouxy.plusone.system.domain.model.account; + +import xyz.zhouxy.plusone.constant.RegexConsts; + +/** + * 值对象:用户名 + * + * @author ZhouXY + */ +public class Username extends Principal { + + public static final String REGEX = RegexConsts.USERNAME; + + private Username(String username) { + super(REGEX); + if (username == null) { + throw new IllegalArgumentException("用户名不能为空"); + } + this.value = username; + if (!isValid()) { + throw new IllegalArgumentException("用户名格式错误"); + } + } + + public static Username of(String username) { + return new Username(username); + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/dict/Dict.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/dict/Dict.java new file mode 100644 index 0000000..627d7ff --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/dict/Dict.java @@ -0,0 +1,129 @@ +package xyz.zhouxy.plusone.system.domain.model.dict; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.ToString; +import xyz.zhouxy.plusone.domain.AggregateRoot; +import xyz.zhouxy.plusone.domain.IWithLabel; +import xyz.zhouxy.plusone.domain.IWithVersion; + +/** + * 聚合根:数据字典 + * + * @author ZhouXY + */ +@ToString +public class Dict extends AggregateRoot implements IWithLabel, IWithVersion { + + private Long id; + private String dictType; + private String dictLabel; + + private Map values = new HashMap<>(); + + private long version; + + // ==================== 领域逻辑 ==================== + + public void addValue(int key, String label) { + if (this.values.containsKey(key)) { + throw new IllegalArgumentException(String.format("字典 %s 已存在值:%d", dictType, key)); + } + this.values.put(key, DictValue.of(key, label)); + } + + public void removeValue(int key) { + this.values.remove(key); + } + + public void updateDict(String dictType, String dictLabel, Map keyLabelMap) { + this.dictType = dictType; + this.dictLabel = dictLabel; + var valueKeys = this.values.keySet(); + for (Integer key : valueKeys) { + if (!keyLabelMap.containsKey(key)) { + this.values.remove(key); + } + } + keyLabelMap.forEach((Integer key, String label) -> { + var temp = this.values.get(key); + if (temp != null) { + temp.label = label; + } else { + this.values.put(key, DictValue.of(key, label)); + } + }); + } + + // ==================== 实例化 ==================== + + Dict(Long id, String dictType, String dictLabel, Set values, long version) { + this.id = id; + this.dictType = dictType; + this.dictLabel = dictLabel; + values.forEach(dictValue -> this.values.put(dictValue.key, dictValue)); + this.version = version; + } + + public static Dict newInstance( + String dictType, + String dictLabel) { + return new Dict(null, dictType, dictLabel, Collections.emptySet(), 0); + } + + public static Dict newInstance( + String dictType, + String dictLabel, + Set values) { + return new Dict(null, dictType, dictLabel, values, 0); + } + + public static Dict newInstance( + String dictType, + String dictLabel, + Map keyLabelMap) { + var values = buildDictValues(keyLabelMap); + return new Dict(null, dictType, dictLabel, values, 0); + } + + private static Set buildDictValues(Map keyLabelMap) { + Set dictValues = new HashSet<>(keyLabelMap.size()); + keyLabelMap.forEach((Integer key, String label) -> dictValues.add(DictValue.of(key, label))); + return dictValues; + } + + // ==================== getters ==================== + + @Override + public Optional getId() { + return Optional.ofNullable(id); + } + + public String getDictType() { + return dictType; + } + + @Override + public String getLabel() { + return this.dictLabel; + } + + public Set getValues() { + return this.values.values().stream().collect(Collectors.toSet()); + } + + @Override + public long getVersion() { + return this.version; + } + + public int count() { + return this.values.size(); + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/dict/DictRepository.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/dict/DictRepository.java new file mode 100644 index 0000000..1859f1e --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/dict/DictRepository.java @@ -0,0 +1,10 @@ +package xyz.zhouxy.plusone.system.domain.model.dict; + +import java.util.List; + +import xyz.zhouxy.plusone.domain.IRepository; + +public interface DictRepository extends IRepository { + + List findAll(); +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/dict/DictValue.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/dict/DictValue.java new file mode 100644 index 0000000..5e8650a --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/dict/DictValue.java @@ -0,0 +1,51 @@ +package xyz.zhouxy.plusone.system.domain.model.dict; + +import java.util.Objects; + +import lombok.Getter; +import lombok.ToString; +import xyz.zhouxy.plusone.domain.IValueObject; + +/** + * 字典值 + * + * @author ZhouXY + */ +@Getter +@ToString +public class DictValue implements IValueObject { + + final Integer key; + String label; + + // ==================== 实例化 ==================== + + private DictValue(int key, String label) { + this.key = key; + this.label = label; + } + + public static DictValue of(int key, String label) { + return new DictValue(key, label); + } + + @Override + public int hashCode() { + return Objects.hash(key); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + DictValue other = (DictValue) obj; + return Objects.equals(key, other.key); + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/Action.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/Action.java new file mode 100644 index 0000000..6f213fd --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/Action.java @@ -0,0 +1,54 @@ +package xyz.zhouxy.plusone.system.domain.model.menu; + +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Getter; +import xyz.zhouxy.plusone.domain.Entity; +import xyz.zhouxy.plusone.domain.IWithLabel; + +/** + * 行为。 + *

+ * 一个 Menu 代表对应一个资源,Action 表示对该资源的行为,每个行为具有对应权限。 + *

+ * + * @author ZhouXY + * + * @see Menu + */ +public class Action extends Entity implements IWithLabel { + + Long id; + String resource; + @Getter + String identifier; + @Getter + String label; + + private Action(Long id, String resource, String identifier, String label) { + this.id = id; + this.resource = resource; + this.identifier = identifier; + this.label = label; + } + + static Action of(Long id, String resource, String identifier, String label) { + return new Action(id, resource, identifier, label); + } + + static Action newInstance(String resource, String identifier, String label) { + return new Action(null, resource, identifier, label); + } + + @JsonProperty("value") + public String value() { + return resource + identifier; + } + + @Override + public Optional getId() { + return Optional.ofNullable(id); + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/Menu.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/Menu.java new file mode 100644 index 0000000..e415593 --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/Menu.java @@ -0,0 +1,140 @@ +package xyz.zhouxy.plusone.system.domain.model.menu; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonValue; + +import lombok.Getter; +import lombok.ToString; +import xyz.zhouxy.plusone.constant.EntityStatus; +import xyz.zhouxy.plusone.domain.AggregateRoot; +import xyz.zhouxy.plusone.domain.IWithOrderNumber; +import xyz.zhouxy.plusone.domain.IWithVersion; + +/** + * 聚合根:菜单 + * + * @author ZhouXY + * + * @see Action + * @see MenuConstructor + */ +@Getter +@ToString +public class Menu extends AggregateRoot implements IWithOrderNumber, IWithVersion { + + MenuType type; + + Long id; + Long parentId; + + String name; + // 若 type 为 MENU_ITEM 且 path 以 http:// 或 https:// 开头则被识别为外链 + String path; + String title; + String icon; + boolean hidden; + int orderNumber; + EntityStatus status; + String remarks; + + // MENU_ITEM + String component; + Boolean cache; + String resource; + private List actions; + + private @Getter long version; + + public void updateMenuInfo( + MenuType type, + long parentId, + String name, + String path, + String title, + String icon, + boolean hidden, + int orderNumber, + EntityStatus status, + String remarks, + String component, + boolean cache, + String resource) { + this.type = type; + this.parentId = parentId; + this.path = path; + this.name = name; + this.title = title; + this.icon = icon; + this.hidden = hidden; + this.orderNumber = orderNumber; + this.status = status; + this.component = component; + this.resource = resource; + this.cache = cache; + this.remarks = remarks; + } + + public Menu addAction(String action, String label) { + return addAction(Action.newInstance(this.resource, action, label)); + } + + public void removeAction(Long actionId) { + this.actions.removeIf(action -> Objects.equals(actionId, action.getId().orElseThrow())); + } + + public void removeAction(String identifier) { + this.actions.removeIf(action -> Objects.equals(identifier, action.identifier)); + } + + @Override + public Optional getId() { + return Optional.ofNullable(id); + } + + @Override + public int getOrderNumber() { + return this.orderNumber; + } + + private Menu addAction(Action action) { + if (this.actions == null) { + this.actions = new ArrayList<>(8); + } + this.actions.add(action); + return this; + } + + Menu(MenuType type, Long id, Long parentId, String name, String path, String title, String icon, + boolean hidden, int orderNumber, EntityStatus status, String remarks, String component, Boolean cache, + String resource, List actions, long version) { + this.type = type; + this.id = id; + this.parentId = parentId; + this.name = name; + this.path = path; + this.title = title; + this.icon = icon; + this.hidden = hidden; + this.orderNumber = orderNumber; + this.status = status; + this.remarks = remarks; + this.component = component; + this.cache = cache; + this.resource = resource; + this.actions = actions; + this.version = version; + } + + public enum MenuType { + MENU_LIST, MENU_ITEM; + + @JsonValue + public int value() { + return ordinal(); + } + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/MenuConstructor.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/MenuConstructor.java new file mode 100644 index 0000000..8902760 --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/MenuConstructor.java @@ -0,0 +1,55 @@ +package xyz.zhouxy.plusone.system.domain.model.menu; + +import java.util.List; + +import xyz.zhouxy.plusone.constant.EntityStatus; +import xyz.zhouxy.plusone.system.domain.model.menu.Menu.MenuType; + +/** + * 菜单构造器。生成新的 MenuList 或 MenuItem + * + * @author ZhouXY + */ +public class MenuConstructor { + + private MenuConstructor() { + throw new IllegalStateException("Utility class"); + } + + public static Menu newMenuItem( + long parentId, + String path, + String name, + String title, + String icon, + boolean hidden, + int orderNumber, + EntityStatus status, + String component, + String resource, + boolean cache, + String remarks) { + List actions = List.of( + Action.newInstance(resource, "-query", "查询"), + Action.newInstance(resource, "-details", "详情"), + Action.newInstance(resource, "-add", "新增"), + Action.newInstance(resource, "-update", "修改"), + Action.newInstance(resource, "-delete", "删除")); + return new Menu(MenuType.MENU_ITEM, null, parentId, name, path, title, icon, hidden, orderNumber, status, + remarks, component, cache, resource, actions, 0L); + } + + public static Menu newMenuList( + long parentId, + String path, + String name, + String title, + String icon, + boolean hidden, + int orderNumber, + EntityStatus status, + String remarks) { + return new Menu(MenuType.MENU_LIST, null, parentId, name, path, title, icon, hidden, orderNumber, status, + remarks, null, null, null, null, 0L); + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/MenuRepository.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/MenuRepository.java new file mode 100644 index 0000000..cd5cbd3 --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/MenuRepository.java @@ -0,0 +1,15 @@ +package xyz.zhouxy.plusone.system.domain.model.menu; + +import java.util.Collection; + +import xyz.zhouxy.plusone.domain.IRepository; + +public interface MenuRepository extends IRepository { + + Collection findByIdIn(Collection ids); + + Collection queryByRoleId(Long roleId); + + Collection findPermissionsByIdIn(Collection permissionIds); + +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/Target.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/Target.java new file mode 100644 index 0000000..c396f48 --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/Target.java @@ -0,0 +1,26 @@ +package xyz.zhouxy.plusone.system.domain.model.menu; + +import com.fasterxml.jackson.annotation.JsonValue; + +import xyz.zhouxy.plusone.domain.IWithLabel; + +public enum Target implements IWithLabel { + BLANK("_blank"), + PARENT("_parent"), + SELF("_self"), + TOP("_top"), + + ; + + private final String label; + + Target(String label) { + this.label = label; + } + + @JsonValue + @Override + public String getLabel() { + return this.label; + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/permission/Action.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/permission/Action.java new file mode 100644 index 0000000..9addc7f --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/permission/Action.java @@ -0,0 +1,47 @@ +package xyz.zhouxy.plusone.system.domain.model.permission; + +import java.util.Optional; + +import lombok.Getter; +import xyz.zhouxy.plusone.domain.Entity; +import xyz.zhouxy.plusone.domain.IWithLabel; +import xyz.zhouxy.plusone.domain.IWithVersion; + +/** + * 行为 + * + * @author ZhouXY + */ +public class Action extends Entity implements IWithLabel, IWithVersion { + + Long id; + String resource; + @Getter String identifier; + @Getter String label; + @Getter long version; + + public Action(Long id, String resource, String identifier, String label, long version) { + this.id = id; + this.resource = resource; + this.identifier = identifier; + this.label = label; + this.version = version; + } + + static Action newInstance(String resource, String identifier, String label) { + return new Action(null, resource, identifier, label, 0L); + } + + static Action existingInstance(Long id, String resource, String action, String label, Long version) { + return new Action(id, resource, action, label, version); + } + + public String value() { + return resource + identifier; + } + + @Override + public Optional getId() { + return Optional.ofNullable(id); + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/permission/Permission.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/permission/Permission.java new file mode 100644 index 0000000..b3bb8cb --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/permission/Permission.java @@ -0,0 +1,69 @@ +package xyz.zhouxy.plusone.system.domain.model.permission; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import lombok.Getter; +import xyz.zhouxy.plusone.domain.AggregateRoot; +import xyz.zhouxy.plusone.domain.IWithVersion; + +/** + * 权限 + * + * @author ZhouXY + */ +public class Permission extends AggregateRoot implements IWithVersion { + + private Long id; + private @Getter String resource; + + private List actions = new ArrayList<>(8); + + private @Getter long version; + + public Permission addAction(String action, String label) { + return addAction(Action.newInstance(resource, action, label)); + } + + public void removeAction(Long actionId) { + this.actions.removeIf(action -> Objects.equals(actionId, action.getId().orElseThrow())); + } + + public void removeAction(String identifier) { + this.actions.removeIf(action -> Objects.equals(identifier, action.identifier)); + } + + // ==================== 实例化 ==================== + + public static Permission newInstance(String resource) { + return new Permission( + null, resource, + List.of(Action.newInstance(resource, ":add", "添加"), + Action.newInstance(resource, ":delete", "删除"), + Action.newInstance(resource, ":update", "更改"), + Action.newInstance(resource, ":query", "查询"), + Action.newInstance(resource, ":details", "详情")), + 0); + } + + Permission(Long id, String resource, List actions, long version) { + this.id = id; + this.resource = resource; + this.actions.addAll(actions); + this.version = version; + } + + // ==================== private ==================== + private Permission addAction(Action action) { + this.actions.add(action); + return this; + } + + // ==================== getter ==================== + @Override + public Optional getId() { + return Optional.ofNullable(id); + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/permission/README.md b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/permission/README.md new file mode 100644 index 0000000..14064f7 --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/permission/README.md @@ -0,0 +1,3 @@ +# Description + +后期考虑菜单项与 Permission 绑定,使之与 Action 解耦,Permission 亦可以单独管理。 \ No newline at end of file diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/ActionRef.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/ActionRef.java new file mode 100644 index 0000000..e03e9cf --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/ActionRef.java @@ -0,0 +1,12 @@ +package xyz.zhouxy.plusone.system.domain.model.role; + +/** + * ActionRef + * + * @author ZhouXY + */ +public record ActionRef(Long actionId) { + public static ActionRef of(Long actionId) { + return new ActionRef(actionId); + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/MenuRef.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/MenuRef.java new file mode 100644 index 0000000..1c7eae6 --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/MenuRef.java @@ -0,0 +1,12 @@ +package xyz.zhouxy.plusone.system.domain.model.role; + +/** + * MenuRef + * + * @author ZhouXY + */ +public record MenuRef(Long menuId) { + public static MenuRef of(Long menuId) { + return new MenuRef(menuId); + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/Role.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/Role.java new file mode 100644 index 0000000..671bba4 --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/Role.java @@ -0,0 +1,112 @@ +package xyz.zhouxy.plusone.system.domain.model.role; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.ToString; +import xyz.zhouxy.plusone.constant.EntityStatus; +import xyz.zhouxy.plusone.domain.AggregateRoot; +import xyz.zhouxy.plusone.domain.IWithVersion; +import xyz.zhouxy.plusone.system.domain.model.menu.Action; +import xyz.zhouxy.plusone.system.domain.model.menu.Menu; + +/** + * 聚合根:角色 + * + * @author ZhouXY + */ +@ToString +public class Role extends AggregateRoot implements IWithVersion { + + private Long id; + private @Getter String name; + private @Getter String identifier; + + private @Getter EntityStatus status; + private @Getter String remarks; + + private final Set menus = new HashSet<>(); + private final Set permissions = new HashSet<>(); + + private @Getter long version; + + public void bindMenus(Set menus) { + this.menus.clear(); + this.menus.addAll(menus); + } + + public void bindPermissions(Set permissions) { + this.permissions.clear(); + this.permissions.addAll(permissions); + } + + public void update( + String name, + String identifier, + EntityStatus status, + String remarks, + Set menus, + Set permissions) { + this.name = name; + this.identifier = identifier; + this.status = status; + this.remarks = remarks; + bindMenus(menus.stream().map(menu -> new MenuRef(menu.getId().orElseThrow())).collect(Collectors.toSet())); + bindPermissions(permissions + .stream() + .map(permission -> new ActionRef(permission.getId().orElseThrow())) + .collect(Collectors.toSet())); + } + + // getters + + @Override + public Optional getId() { + return Optional.ofNullable(id); + } + + public Set getMenus() { + return Set.copyOf(menus); + } + + public Set getPermissions() { + return Set.copyOf(permissions); + } + + public static Role newInstance( + String name, + String identifier, + EntityStatus status, + String remarks, + Set menus, + Set permissions) { + return new Role(null, name, identifier, status, remarks, menus, permissions, 0L); + } + + /** + * 构造方法 + * + * @param id id + * @param name 角色名 + * @param identifier 标识符 + * @param status 状态 + * @param remarks 备注 + * @param menus 菜单 + * @param permissions 权限 + * @param version 版本号 + */ + Role(Long id, String name, String identifier, EntityStatus status, String remarks, + Set menus, Set permissions, long version) { + this.id = id; + this.name = name; + this.identifier = identifier; + this.status = status; + this.remarks = remarks; + bindMenus(menus); + bindPermissions(permissions); + this.version = version; + } +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/RoleRepository.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/RoleRepository.java new file mode 100644 index 0000000..de3ba14 --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/RoleRepository.java @@ -0,0 +1,11 @@ +package xyz.zhouxy.plusone.system.domain.model.role; + +import java.util.Collection; + +import xyz.zhouxy.plusone.domain.IRepository; + +public interface RoleRepository extends IRepository { + + Collection findByAccountId(Long accountId); + +} diff --git a/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/service/MenuService.java b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/service/MenuService.java new file mode 100644 index 0000000..6ad4939 --- /dev/null +++ b/plusone-system/plusone-system-domain/src/main/java/xyz/zhouxy/plusone/system/domain/service/MenuService.java @@ -0,0 +1,44 @@ +package xyz.zhouxy.plusone.system.domain.service; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; + +import xyz.zhouxy.plusone.system.domain.model.menu.Menu; +import xyz.zhouxy.plusone.system.domain.model.menu.MenuRepository; +import xyz.zhouxy.plusone.system.domain.model.role.MenuRef; +import xyz.zhouxy.plusone.system.domain.model.role.Role; +import xyz.zhouxy.plusone.system.domain.model.role.RoleRepository; + +/** + * 领域服务:菜单服务 + * + * @author ZhouXY + */ +@Service +public class MenuService { + + private final RoleRepository roleRepository; + private final MenuRepository menuRepository; + + MenuService(RoleRepository roleRepository, MenuRepository menuRepository) { + this.roleRepository = roleRepository; + this.menuRepository = menuRepository; + } + + /** + * 根据账号 id 查询菜单列表(不形成树结构) + * @param accountId + * @return + */ + public Collection queryAllMenuListByAccountId(Long accountId) { + Collection roles = roleRepository.findByAccountId(accountId); + Set menuRefs = new HashSet<>(); + roles.forEach(role -> menuRefs.addAll(role.getMenus())); + return menuRepository.findByIdIn(menuRefs.stream().map(MenuRef::menuId).collect(Collectors.toSet())); + } + +} diff --git a/plusone-system/plusone-system-domain/src/test/java/xyz/zhouxy/plusone/system/domain/model/account/PasswordTests.java b/plusone-system/plusone-system-domain/src/test/java/xyz/zhouxy/plusone/system/domain/model/account/PasswordTests.java new file mode 100644 index 0000000..5e943f0 --- /dev/null +++ b/plusone-system/plusone-system-domain/src/test/java/xyz/zhouxy/plusone/system/domain/model/account/PasswordTests.java @@ -0,0 +1,18 @@ +package xyz.zhouxy.plusone.system.domain.model.account; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class PasswordTests { + + @Test + void testNewDefaultPassword() { + var pwd = Password.newDefaultPassword(); + log.debug("value -- {}; salt -- {}", pwd.value(), pwd.getSalt()); + assertTrue(pwd.check("A1b2C3d4"), "默认密码校验失败"); + } +} diff --git a/plusone-system/plusone-system-infrastructure/pom.xml b/plusone-system/plusone-system-infrastructure/pom.xml new file mode 100644 index 0000000..1b6f18e --- /dev/null +++ b/plusone-system/plusone-system-infrastructure/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + plusone-system + xyz.zhouxy + 1.0.0-SNAPSHOT + + + plusone-system-infrastructure + + + + xyz.zhouxy + plusone-basic-infrastructure + + + xyz.zhouxy + plusone-system-domain + + + + diff --git a/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/AccountRepositoryImpl.java b/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/AccountRepositoryImpl.java new file mode 100644 index 0000000..d4bfa84 --- /dev/null +++ b/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/AccountRepositoryImpl.java @@ -0,0 +1,218 @@ +package xyz.zhouxy.plusone.system.domain.model.account; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Objects; + +import javax.annotation.Nonnull; + +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.stereotype.Repository; + +import cn.hutool.core.util.IdUtil; +import xyz.zhouxy.plusone.jdbc.JdbcRepositorySupport; +import xyz.zhouxy.plusone.util.AssertResult; + +/** + * AccountRepository 实现类 + * + * @author ZhouXY + */ +@Repository +public class AccountRepositoryImpl extends JdbcRepositorySupport implements AccountRepository { + + private final AccountRoleRefDAO accountRoleDAO; + + public AccountRepositoryImpl(@Nonnull NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + super(namedParameterJdbcTemplate); + this.accountRoleDAO = new AccountRoleRefDAO(namedParameterJdbcTemplate); + } + + @Override + protected final void doDelete(@Nonnull Account entity) { + int i = this.jdbc.update(""" + UPDATE sys_account SET deleted = id, "version" = "version" + 1 + WHERE id = :id AND deleted = 0 AND "version" = :version + """, + new MapSqlParameterSource() + .addValue("id", entity.getId().orElseThrow()) + .addValue("version", entity.getVersion())); + AssertResult.update(i, 1); + } + + @Override + protected final Account doFindById(@Nonnull Long id) { + return queryForObject(""" + SELECT + id, email, mobile_phone, username, "password", salt, avatar, sex, nickname, status, + created_by, updated_by, "version" + FROM sys_account + WHERE id = :id AND deleted = 0 + """, + new MapSqlParameterSource("id", id)); + } + + @Override + public Account findByEmail(Email email) { + return queryForObject(""" + SELECT + id, email, mobile_phone, username, "password", salt, avatar, sex, nickname, status, + created_by, updated_by, "version" + FROM sys_account + WHERE email = :email AND deleted = 0 + """, + new MapSqlParameterSource("email", email.value())); + } + + @Override + public Account findByMobilePhone(MobilePhone mobilePhone) { + return queryForObject(""" + SELECT + id, email, mobile_phone, username, "password", salt, avatar, sex, nickname, status, + created_by, updated_by, "version" + FROM sys_account + WHERE mobile_phone = :mobilePhone AND deleted = 0 + """, + new MapSqlParameterSource("mobilePhone", mobilePhone.value())); + } + + @Override + public Account findByUsername(Username username) { + return queryForObject(""" + SELECT + id, email, mobile_phone, username, "password", salt, avatar, sex, nickname, status, + created_by, updated_by, "version" + FROM sys_account + WHERE username = :username AND deleted = 0 + """, + new MapSqlParameterSource("username", username.value())); + } + + @Override + public boolean exists(Long id) { + return queryExists("SELECT 1 FROM sys_account WHERE id = :id AND deleted = 0 LIMIT 1", + new MapSqlParameterSource("id", id)); + } + + @Override + public boolean existsUsername(Username username) { + return queryExists("SELECT 1 FROM sys_account WHERE username = :username AND deleted = 0 LIMIT 1", + new MapSqlParameterSource("username", username.value())); + } + + @Override + public boolean existsEmail(Email email) { + return queryExists("SELECT 1 FROM sys_account WHERE email = :email AND deleted = 0 LIMIT 1", + new MapSqlParameterSource("email", email.value())); + } + + @Override + public boolean existsMobilePhone(MobilePhone mobilePhone) { + return queryExists("SELECT 1 FROM sys_account WHERE mobile_phone = :mobile_phone AND deleted = 0 LIMIT 1", + new MapSqlParameterSource("mobile_phone", mobilePhone.value())); + } + + @Override + public Collection findByRoleId(Long roleId) { + return queryForList(""" + SELECT + a.id, a.email, a.mobile_phone, a.username, a."password", a.salt, + a.avatar, a.sex, a.nickname, a.status, + a.created_by, a.updated_by, a."version" + FROM sys_account a + LEFT JOIN sys_account_role ar ON a.id = ar.account_id + WHERE ar.role_id = :roleId AND a.deleted = 0 + """, + new MapSqlParameterSource("roleId", roleId)); + } + + @Override + protected final Account doInsert(@Nonnull Account entity) { + String sql = """ + INSERT INTO sys_account + (id, email, mobile_phone, username, "password", salt, avatar, sex, nickname, status, created_by, create_time) + VALUES + (:id, :email, :mobilePhone, :username, :password, :salt, :avatar, :sex, :nickname, :status, :createdBy, :createTime) + """; + long id = IdUtil.getSnowflakeNextId(); + SqlParameterSource params = generateParamSource(id, entity); + int i = jdbc.update(sql, params); + AssertResult.update(i, 1); + this.accountRoleDAO.insertAccountRoleRefs(id, entity.getRoleIds()); + return entity; + } + + @Override + protected final Account doUpdate(@Nonnull Account entity) { + String sql = """ + UPDATE sys_account + SET "email" = :email, + "mobile_phone" = :mobilePhone, + "username" = :username, + "password" = :password, + "salt" = :salt, + "avatar" = :avatar, + "sex" = :sex, + "nickname" = :nickname, + "status" = :status, + "updated_by" = :updatedBy, + "update_time" = :updateTime, + "version" = "version" + 1 + WHERE id = :id AND deleted = 0 AND "version" = :version + """; + SqlParameterSource params = generateParamSource(entity); + int i = this.jdbc.update(sql, params); + AssertResult.update(i, 1); + this.accountRoleDAO.saveAccountRoleRefs(entity); + return entity; + } + + @Override + protected final Account mapRow(ResultSet rs) throws SQLException { + long accountId = rs.getLong("id"); + AccountInfo accountInfo = AccountInfo.of( + rs.getString("nickname"), + rs.getString("avatar"), + Sex.of(rs.getInt("sex"))); + return new Account( + accountId, + rs.getString("username"), + rs.getString("email"), + rs.getString("mobile_phone"), + Password.of(rs.getString("password"), rs.getString("salt")), + AccountStatus.of(rs.getInt("status")), + accountInfo, + this.accountRoleDAO.selectRoleIdsByAccountId(accountId), + rs.getLong("created_by"), + rs.getLong("updated_by"), + rs.getLong("version")); + } + + @Override + protected final SqlParameterSource generateParamSource(Long id, @Nonnull Account entity) { + LocalDateTime now = LocalDateTime.now(); + AccountInfo accountInfo = entity.getAccountInfo(); + return new MapSqlParameterSource() + .addValue("id", id) + .addValue("email", Objects.nonNull(entity.getEmail()) ? entity.getEmail().value() : null) + .addValue("mobilePhone", + Objects.nonNull(entity.getMobilePhone()) ? entity.getMobilePhone().value() : null) + .addValue("username", entity.getUsername().value()) + .addValue("password", entity.getPassword().value()) + .addValue("salt", entity.getPassword().getSalt()) + .addValue("avatar", accountInfo.getAvatar().toString()) + .addValue("sex", accountInfo.getSex().getValue()) + .addValue("nickname", + Objects.nonNull(accountInfo.getNickname()) ? accountInfo.getNickname().value() : null) + .addValue("status", entity.getStatus().getValue()) + .addValue("createdBy", entity.getCreatedBy()) + .addValue("createTime", now) + .addValue("updatedBy", entity.getUpdatedBy()) + .addValue("updateTime", now) + .addValue("version", entity.getVersion()); + } +} diff --git a/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/AccountRoleRefDAO.java b/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/AccountRoleRefDAO.java new file mode 100644 index 0000000..bae5edb --- /dev/null +++ b/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/account/AccountRoleRefDAO.java @@ -0,0 +1,53 @@ +package xyz.zhouxy.plusone.system.domain.model.account; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; + +import xyz.zhouxy.plusone.util.AssertResult; +import xyz.zhouxy.plusone.util.NumberUtil; + +class AccountRoleRefDAO { + private final NamedParameterJdbcTemplate jdbc; + + AccountRoleRefDAO(NamedParameterJdbcTemplate jdbc) { + this.jdbc = jdbc; + } + + Set selectRoleIdsByAccountId(Long accountId) { + List roleRefs = this.jdbc.queryForList(""" + SELECT r.id FROM sys_role r RIGHT JOIN sys_account_role ar ON r.id = ar.role_id + WHERE r.deleted = 0 AND ar.account_id = :accountId; + """, + new MapSqlParameterSource("accountId", accountId), + Long.TYPE); + return new HashSet<>(roleRefs); + } + + void clearAccountRoleRefs(Account entity) { + var param = new MapSqlParameterSource("accountId", entity.getId().orElseThrow()); + this.jdbc.update("DELETE FROM sys_account_role WHERE account_id = :accountId", param); + } + + void insertAccountRoleRefs(Long accountId, Set roleRefs) { + String sql = "INSERT INTO sys_account_role (account_id, role_id) VALUES (:accountId, :roleId)"; + MapSqlParameterSource[] batchArgs = roleRefs + .stream() + .map((Long roleId) -> new MapSqlParameterSource() + .addValue("accountId", accountId) + .addValue("roleId", roleId)) + .toArray(MapSqlParameterSource[]::new); + int[] i = this.jdbc.batchUpdate(sql, batchArgs); + AssertResult.update(roleRefs.size(), NumberUtil.sum(i)); + } + + void saveAccountRoleRefs(Account entity) { + Long accountId = entity.getId().orElseThrow(); + Set roleRefs = entity.getRoleIds(); + clearAccountRoleRefs(entity); + insertAccountRoleRefs(accountId, roleRefs); + } +} diff --git a/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/dict/DictRepositoryImpl.java b/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/dict/DictRepositoryImpl.java new file mode 100644 index 0000000..f501edd --- /dev/null +++ b/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/dict/DictRepositoryImpl.java @@ -0,0 +1,118 @@ +package xyz.zhouxy.plusone.system.domain.model.dict; + +import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDateTime; +import java.util.List; + +import javax.annotation.Nonnull; + +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.stereotype.Repository; + +import cn.hutool.core.util.IdUtil; +import xyz.zhouxy.plusone.jdbc.JdbcRepositorySupport; +import xyz.zhouxy.plusone.util.AssertResult; + +/** + * DictRepository 实现类 + * + * @author ZhouXY + */ +@Repository +public class DictRepositoryImpl extends JdbcRepositorySupport implements DictRepository { + + private final DictValueDAO dictValueDAO; + + public DictRepositoryImpl(@Nonnull NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + super(namedParameterJdbcTemplate); + this.dictValueDAO = new DictValueDAO(namedParameterJdbcTemplate); + } + + @Override + public Dict doFindById(@Nonnull Long id) { + return queryForObject("SELECT id, dict_type, dict_label, \"version\" WHERE id = :id AND deleted = 0", + new MapSqlParameterSource("id", id)); + } + + @Override + protected final Dict doInsert(@Nonnull Dict entity) { + long id = IdUtil.getSnowflakeNextId(); + int i = this.jdbc.update(""" + INSERT INTO sys_dict_type (dict_type, dict_label, create_time, created_by) + VALUES (:dictType, :dictLabel, :createTime, :createdBy) + """, + generateParamSource(id, entity)); + AssertResult.update(i, 1); + this.dictValueDAO.insertDictValues(id, entity); + return find(id); + } + + @Override + protected final Dict doUpdate(@Nonnull Dict entity) { + int i = this.jdbc.update(""" + UPDATE sys_dict_type + SET dict_type = :dictType, + dict_label = :dictLabel, + update_time = :updateTime, + updated_by = :updatedBy, + "version" = "version" + 1 + WHERE id = :id AND deleted = 0 AND "version" = :version + """, + generateParamSource(entity)); + AssertResult.update(i, 1); + this.dictValueDAO.updateDictValues(entity); + return find(entity.getId().orElseThrow()); + } + + @Override + protected final void doDelete(@Nonnull Dict entity) { + int i = this.jdbc.update(""" + UPDATE sys_dict_type SET deleted = id, "version" = "version" + 1 + WHERE id = :id AND deleted = 0 AND "version" = :version + """, + generateParamSource(entity)); + AssertResult.update(i, 1); + } + + @Override + public boolean exists(Long id) { + return queryExists("SELECT 1 FROM sys_dict_type WHERE id = :id AND deleted = 0 LIMIT 1", + new MapSqlParameterSource("id", id)); + } + + @Override + public List findAll() { + return queryForList("SELECT id, dict_type, dict_label, \"version\" WHERE deleted = 0"); + } + + @Override + protected final Dict mapRow(ResultSet rs) throws SQLException { + long id = rs.getLong("id"); + return new Dict( + id, + rs.getString("dict_type"), + rs.getString("dict_label"), + this.dictValueDAO.selectDictValuesByDictId(id), + rs.getLong("version")); + } + + @Override + protected final SqlParameterSource generateParamSource(Long id, @Nonnull Dict entity) { + LocalDateTime now = LocalDateTime.now(); + long loginId = adminAuthLogic.getLoginIdAsLong(); + return new MapSqlParameterSource() + .addValue("dictType", entity.getDictType()) + .addValue("dictLabel", entity.getLabel()) + .addValue("createTime", now) + .addValue("createdBy", loginId) + .addValue("updateTime", now) + .addValue("updatedBy", loginId) + .addValue("id", id) + .addValue("version", entity.getVersion()); + } +} diff --git a/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/dict/DictValueDAO.java b/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/dict/DictValueDAO.java new file mode 100644 index 0000000..0708afd --- /dev/null +++ b/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/dict/DictValueDAO.java @@ -0,0 +1,51 @@ +package xyz.zhouxy.plusone.system.domain.model.dict; + +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.util.CollectionUtils; + +import xyz.zhouxy.plusone.util.AssertResult; +import xyz.zhouxy.plusone.util.NumberUtil; + +class DictValueDAO { + private final NamedParameterJdbcTemplate jdbc; + + DictValueDAO(NamedParameterJdbcTemplate jdbc) { + this.jdbc = jdbc; + } + + void updateDictValues(Dict entity) { + MapSqlParameterSource deleteParam = new MapSqlParameterSource("dictType", entity.getId().orElseThrow()); + this.jdbc.update("DELETE FROM sys_dict_value WHERE dict_type = :dictType", deleteParam); + int i = insertDictValues(entity.getId().orElseThrow(), entity); + AssertResult.update(i, entity.count()); + } + + int insertDictValues(Long dictId, Dict entity) { + if (Objects.isNull(dictId) || Objects.isNull(entity) || CollectionUtils.isEmpty(entity.getValues())) { + return 0; + } + int[] i = this.jdbc.batchUpdate( + "INSERT INTO sys_dict_value (dict_type, dict_key, label) VALUES (:dictType, :dictKey, :label)", + entity.getValues().stream() + .map(dictValue -> new MapSqlParameterSource() + .addValue("dictType", dictId) + .addValue("dictKey", dictValue.getKey()) + .addValue("label", dictValue.getLabel())) + .toArray(SqlParameterSource[]::new)); + return NumberUtil.sum(i); + } + + Set selectDictValuesByDictId(long id) { + return this.jdbc.queryForStream(""" + SELECT dict_key, label FROM sys_dict_value WHERE dict_type = :dictType + """, new MapSqlParameterSource("dictType", id), + (rs, rowNum) -> DictValue.of(rs.getInt("dict_key"), rs.getString("label"))) + .collect(Collectors.toSet()); + } +} diff --git a/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/ActionDAO.java b/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/ActionDAO.java new file mode 100644 index 0000000..68dd5dd --- /dev/null +++ b/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/ActionDAO.java @@ -0,0 +1,123 @@ +package xyz.zhouxy.plusone.system.domain.model.menu; + +import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.annotation.Nonnull; + +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; + +import cn.hutool.core.util.IdUtil; +import xyz.zhouxy.plusone.jdbc.JdbcEntityDaoSupport; + +/** + * {@link Action} 的数据访问对象 + * + * @author ZhouXY + * @date 2022-10-31 12:31:49 + */ +class ActionDAO extends JdbcEntityDaoSupport { + + ActionDAO(@Nonnull NamedParameterJdbcTemplate jdbc) { + super(jdbc); + } + + void saveActions(Long menuId, List actions) { + // 删除要删除的权限 + Collection ids = actions.stream() + .filter(action -> action.getId().isPresent()) + .map(action -> action.getId().orElseThrow()) + .collect(Collectors.toSet()); + if (!ids.isEmpty()) { + this.jdbc.update( + "UPDATE sys_action SET deleted = id WHERE resource = :resource AND id NOT IN (:ids) AND deleted = 0", + new MapSqlParameterSource() + .addValue("resource", menuId) + .addValue("ids", ids)); + } + + // 更新存在的数据 + this.jdbc.batchUpdate(""" + UPDATE sys_action + SET resource = :resource, + identifier = :identifier, + label = :label, + update_time = :updateTime, + updated_by = :updatedBy + WHERE id = :id AND deleted = 0 + """, + actions.stream() + .filter(action -> action.getId().isPresent()) + .map(action -> generateParamSource(menuId, action)) + .toArray(MapSqlParameterSource[]::new)); + + // 插入新添加的数据 + this.jdbc.batchUpdate(""" + INSERT INTO sys_action + (id, resource, identifier, "label", create_time, created_by) + VALUES + (:id, :resource, :identifier, :label, :createTime, :createdBy) + """, + actions.stream() + .filter(action -> action.getId().isEmpty()) + .map(action -> generateParamSource(menuId, IdUtil.getSnowflakeNextId(), action)) + .toArray(MapSqlParameterSource[]::new)); + } + + List selectActionsByMenuId(long menuId) { + return queryForList(""" + SELECT a.id, m.resource, a.identifier, a.label + FROM sys_action a + JOIN (SELECT id, resource FROM sys_menu WHERE id = :menuId AND deleted = 0) m ON a.resource = m.id + WHERE a.deleted = 0 + """, new MapSqlParameterSource("menuId", menuId)); + } + + Collection selectActionsByIdIn(Collection actionIds) { + if (Objects.isNull(actionIds) || actionIds.isEmpty()) { + return Collections.emptyList(); + } + return queryForList(""" + SELECT a.id, m.resource, a.identifier, a.label + FROM sys_action a + LEFT JOIN sys_menu m ON a.resource = m.id + WHERE a.id IN (:actionIds) AND a.deleted = 0 + """, new MapSqlParameterSource("actionIds", actionIds)); + } + + private SqlParameterSource generateParamSource(Long menuId, Action action) { + return generateParamSource(menuId, action.getId().orElseThrow(), action); + } + + private SqlParameterSource generateParamSource(Long menuId, Long actionId, Action action) { + long loginId = adminAuthLogic.getLoginIdAsLong(); + LocalDateTime now = LocalDateTime.now(); + return new MapSqlParameterSource("id", actionId) + .addValue("resource", menuId) + .addValue("identifier", action.getIdentifier()) + .addValue("label", action.getLabel()) + .addValue("createTime", now) + .addValue("createdBy", loginId) + .addValue("updateTime", now) + .addValue("updatedBy", loginId); + } + + @Override + protected Action mapRow(ResultSet rs) throws SQLException { + return Action.of( + rs.getLong("id"), + rs.getString("resource"), + rs.getString("identifier"), + rs.getString("label")); + } +} diff --git a/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/MenuRepositoryImpl.java b/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/MenuRepositoryImpl.java new file mode 100644 index 0000000..0967990 --- /dev/null +++ b/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/menu/MenuRepositoryImpl.java @@ -0,0 +1,200 @@ +package xyz.zhouxy.plusone.system.domain.model.menu; + +import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; + +import javax.annotation.Nonnull; + +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; + +import cn.hutool.core.util.IdUtil; +import xyz.zhouxy.plusone.constant.EntityStatus; +import xyz.zhouxy.plusone.jdbc.JdbcRepositorySupport; +import xyz.zhouxy.plusone.system.domain.model.menu.Menu.MenuType; +import xyz.zhouxy.plusone.util.AssertResult; +import xyz.zhouxy.plusone.util.EnumUtil; + +/** + * MenuRepository 实现类 + * + * @author ZhouXY + */ +@Repository +public class MenuRepositoryImpl extends JdbcRepositorySupport implements MenuRepository { + + private final ActionDAO actionDAO; + + MenuRepositoryImpl(@Nonnull NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + super(namedParameterJdbcTemplate); + this.actionDAO = new ActionDAO(namedParameterJdbcTemplate); + } + + @Override + protected final Menu doFindById(@Nonnull Long id) { + return queryForObject(""" + SELECT + id, parent_id, "type", "name", "path", title, icon, hidden, order_number, status, remarks, + component, "cache", resource, "version" + FROM sys_menu + WHERE id = :id AND deleted = 0 + """, + new MapSqlParameterSource("id", id)); + } + + @Override + protected final Menu doInsert(@Nonnull Menu entity) { + long id = IdUtil.getSnowflakeNextId(); + String sql = """ + INSERT INTO sys_menu ( + id, parent_id, "type", name, "path", title, icon, hidden, order_number, status, remarks, + component, "cache", resource, create_time, created_by) + VALUES + (:id, :parentId, :type, :name, :path, :title, :icon, :hidden, :orderNumber, :status, :remarks, + :component, :cache, :resource, :createTime, :createdBy) + """; + MapSqlParameterSource paramSource = generateParamSource(id, entity); + int i = this.jdbc.update(sql, paramSource); + AssertResult.update(i, 1); + this.actionDAO.saveActions(id, entity.getActions()); + return entity; + } + + @Override + protected final Menu doUpdate(@Nonnull Menu entity) { + String sql = """ + UPDATE sys_menu + SET "parent_id" = :parentId, + "type" = :type, + "name" = :name, + "path" = :path, + "title" = :title, + "icon" = :icon, + "hidden" = :hidden, + "order_number" = :orderNumber, + "status" = :status, + "remarks" = :remarks, + "component" = :component, + "cache" = :cache, + "resource" = :resource, + "update_time" = :updateTime, + "updated_by" = :updatedBy, + "version" = "version" + 1 + WHERE id = :id AND deleted = 0 AND "version" = :version + """; + + // 更新菜单 + int i = this.jdbc.update(sql, generateParamSource(entity)); + AssertResult.update(i, 1); + + // 保存权限 + Long id = entity.getId().orElseThrow(); + this.actionDAO.saveActions(id, entity.getActions()); + return entity; + } + + @Override + protected final void doDelete(@Nonnull Menu entity) { + int i = this.jdbc.update(""" + UPDATE sys_menu SET deleted = id, "version" = "version" + 1 + WHERE id = :id AND deleted = 0 AND "version" = :version + """, + new MapSqlParameterSource("id", entity.getId().orElseThrow()) + .addValue("version", entity.getVersion())); + AssertResult.update(i, 1); + } + + @Override + public boolean exists(Long id) { + return queryExists("SELECT 1 FROM sys_menu WHERE id = :id AND deleted = 0 LIMIT 1", + new MapSqlParameterSource("id", id)); + } + + @Override + public Collection findByIdIn(Collection ids) { + if (Objects.isNull(ids) || ids.isEmpty()) { + return Collections.emptyList(); + } + return queryForList(""" + SELECT + id, parent_id, "type", "name", "path", title, icon, hidden, order_number, status, remarks, + component, "cache", resource, "version" + FROM sys_menu + WHERE id IN (:ids) AND deleted = 0 + """, + new MapSqlParameterSource("ids", ids)); + } + + @Override + public Collection findPermissionsByIdIn(Collection permissionIds) { + return this.actionDAO.selectActionsByIdIn(permissionIds); + } + + @Override + public Collection queryByRoleId(Long roleId) { + return queryForList(""" + SELECT + m.id, m.parent_id, m."type", m."name", m."path", m.title, m.icon, m.hidden, m.order_number, + m.status, m.remarks, m.component, m."cache", m.resource, m."version" + FROM sys_menu AS m + LEFT JOIN sys_role_menu AS rm ON m.id = rm.menu_id + WHERE rm.role_id = :roleId AND r.deleted = 0 + """, + new MapSqlParameterSource("roleId", roleId)); + } + + @Override + protected final Menu mapRow(ResultSet rs) throws SQLException { + long menuId = rs.getLong("id"); + return new Menu( + EnumUtil.valueOf(MenuType.class, rs.getInt("type")), + menuId, + rs.getLong("parent_id"), + rs.getString("name"), + rs.getString("path"), + rs.getString("title"), + rs.getString("icon"), + rs.getBoolean("hidden"), + rs.getInt("order_number"), + EntityStatus.of(rs.getInt("status")), + rs.getString("remarks"), + rs.getString("component"), + rs.getBoolean("cache"), + rs.getString("resource"), + this.actionDAO.selectActionsByMenuId(menuId), + rs.getLong("version")); + } + + @Override + protected final MapSqlParameterSource generateParamSource(Long id, @Nonnull Menu entity) { + LocalDateTime now = LocalDateTime.now(); + long loginId = adminAuthLogic.getLoginIdAsLong(); + return new MapSqlParameterSource() + .addValue("id", id) + .addValue("parentId", entity.getParentId()) + .addValue("type", entity.getType().value()) + .addValue("name", entity.getName()) + .addValue("path", entity.getPath()) + .addValue("title", entity.getTitle()) + .addValue("icon", entity.getIcon()) + .addValue("hidden", entity.isHidden()) + .addValue("orderNumber", entity.getOrderNumber()) + .addValue("status", entity.getStatus().getValue()) + .addValue("remarks", entity.getRemarks()) + .addValue("component", entity.getComponent()) + .addValue("cache", entity.getCache()) + .addValue("resource", entity.getResource()) + .addValue("createTime", now) + .addValue("createdBy", loginId) + .addValue("updateTime", now) + .addValue("updatedBy", loginId) + .addValue("version", entity.getVersion()); + } +} diff --git a/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/RoleMenuRefDAO.java b/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/RoleMenuRefDAO.java new file mode 100644 index 0000000..07263b4 --- /dev/null +++ b/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/RoleMenuRefDAO.java @@ -0,0 +1,45 @@ +package xyz.zhouxy.plusone.system.domain.model.role; + +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; + +import xyz.zhouxy.plusone.util.AssertResult; +import xyz.zhouxy.plusone.util.NumberUtil; + +class RoleMenuRefDAO { + + private final NamedParameterJdbcTemplate jdbc; + + public RoleMenuRefDAO(NamedParameterJdbcTemplate jdbc) { + this.jdbc = jdbc; + } + + Set selectMenuRefsByRoleId(long roleId) { + return this.jdbc.queryForList("SELECT menu_id FROM sys_role_menu WHERE role_id = :roleId", + new MapSqlParameterSource("roleId", roleId), + Long.TYPE) + .stream() + .map(MenuRef::of) + .collect(Collectors.toSet()); + } + + void clearRoleMenuRefs(Role entity) { + MapSqlParameterSource param = new MapSqlParameterSource("roleId", entity.getId().orElseThrow()); + this.jdbc.update("DELETE FROM sys_role_menu WHERE role_id = :roleId", param); + } + + void saveRoleMenuRefs(Long roleId, Role entity) { + String sql = "INSERT INTO sys_role_menu(role_id, menu_id) VALUES (:roleId, :menuId)"; + MapSqlParameterSource[] batchArgs = entity.getMenus() + .stream() + .map(menuRef -> new MapSqlParameterSource() + .addValue("roleId", roleId) + .addValue("menuId", menuRef.menuId())) + .toArray(MapSqlParameterSource[]::new); + int[] i = this.jdbc.batchUpdate(sql, batchArgs); + AssertResult.update(entity.getMenus().size(), NumberUtil.sum(i)); + } +} diff --git a/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/RolePermissionRefDAO.java b/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/RolePermissionRefDAO.java new file mode 100644 index 0000000..7808495 --- /dev/null +++ b/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/RolePermissionRefDAO.java @@ -0,0 +1,45 @@ +package xyz.zhouxy.plusone.system.domain.model.role; + +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; + +import xyz.zhouxy.plusone.util.AssertResult; +import xyz.zhouxy.plusone.util.NumberUtil; + +class RolePermissionRefDAO { + private final NamedParameterJdbcTemplate jdbc; + + public RolePermissionRefDAO(NamedParameterJdbcTemplate jdbc) { + this.jdbc = jdbc; + } + + Set selectPermissionRefsByRoleId(long roleId) { + return this.jdbc.queryForList("SELECT permission_id FROM sys_role_permission WHERE role_id = :roleId", + new MapSqlParameterSource("roleId", roleId), + Long.TYPE) + .stream() + .map(ActionRef::of) + .collect(Collectors.toSet()); + } + + void clearRolePermissionRefs(Role entity) { + MapSqlParameterSource param = new MapSqlParameterSource("roleId", entity.getId().orElseThrow()); + this.jdbc.update("DELETE FROM sys_role_permission WHERE role_id = :roleId", param); + } + + void saveRolePermissionRefs(Long roleId, Role entity) { + String sql = "INSERT INTO sys_role_permission(role_id, permission_id) VALUES (:roleId, :permissionId)"; + SqlParameterSource[] batchArgs = entity.getPermissions() + .stream() + .map(menuRef -> new MapSqlParameterSource() + .addValue("roleId", roleId) + .addValue("permissionId", menuRef.actionId())) + .toArray(MapSqlParameterSource[]::new); + int[] i = this.jdbc.batchUpdate(sql, batchArgs); + AssertResult.update(entity.getMenus().size(), NumberUtil.sum(i)); + } +} diff --git a/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/RoleRepositoryImpl.java b/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/RoleRepositoryImpl.java new file mode 100644 index 0000000..d077920 --- /dev/null +++ b/plusone-system/plusone-system-infrastructure/src/main/java/xyz/zhouxy/plusone/system/domain/model/role/RoleRepositoryImpl.java @@ -0,0 +1,142 @@ +package xyz.zhouxy.plusone.system.domain.model.role; + +import static xyz.zhouxy.plusone.system.constant.AuthLogic.adminAuthLogic; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDateTime; +import java.util.Collection; + +import javax.annotation.Nonnull; + +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; + +import cn.hutool.core.util.IdUtil; +import xyz.zhouxy.plusone.constant.EntityStatus; +import xyz.zhouxy.plusone.jdbc.JdbcRepositorySupport; +import xyz.zhouxy.plusone.util.AssertResult; + +/** + * RoleRepository 实现类 + * + * @author ZhouXY + */ +@Repository +public class RoleRepositoryImpl extends JdbcRepositorySupport implements RoleRepository { + + private final RoleMenuRefDAO roleMenuRefDAO; + private final RolePermissionRefDAO rolePermissionRefDAO; + + RoleRepositoryImpl(@Nonnull NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + super(namedParameterJdbcTemplate); + this.roleMenuRefDAO = new RoleMenuRefDAO(namedParameterJdbcTemplate); + this.rolePermissionRefDAO = new RolePermissionRefDAO(namedParameterJdbcTemplate); + } + + @Override + protected final Role doFindById(@Nonnull Long id) { + return queryForObject(""" + SELECT "id","name","identifier","status","remarks","version" + FROM "sys_role" + WHERE id = :id AND deleted = 0 + """, new MapSqlParameterSource("id", id)); + } + + @Override + protected final void doDelete(@Nonnull Role entity) { + int i = this.jdbc.update(""" + UPDATE sys_account SET deleted = id, "version" = "version" + 1 + WHERE id = :id AND deleted = 0 AND "version" = :version + """, + new MapSqlParameterSource("id", entity.getId().orElseThrow()) + .addValue("version", entity.getVersion())); + AssertResult.update(i, 1); + } + + @Override + public boolean exists(Long id) { + return queryExists("SELECT 1 FROM sys_role WHERE id = :id AND deleted = 0 LIMIT 1", + new MapSqlParameterSource("id", id)); + } + + @Override + protected final Role doInsert(@Nonnull Role entity) { + long id = IdUtil.getSnowflakeNextId(); + int i = this.jdbc.update(""" + INSERT INTO sys_role + (id, "name", identifier, status, remarks, create_time, created_by) + VALUES + (:id, :name, :identifier, :status, :remarks, :createTime, :createdBy) + """, generateParamSource(id, entity)); + AssertResult.update(i, 1); + this.roleMenuRefDAO.saveRoleMenuRefs(id, entity); + this.rolePermissionRefDAO.saveRolePermissionRefs(id, entity); + return entity; + } + + @Override + protected final Role doUpdate(@Nonnull Role entity) { + int i = this.jdbc.update(""" + UPDATE sys_role + SET "name" = :name, + "identifier" = :identifier, + "status" = :status, + "remarks" = :remarks, + "update_time" = :updateTime, + "updated_by" = :updatedBy, + "version" = "version" + 1 + WHERE id = :id AND deleted = 0 AND "version" = :version + """, generateParamSource(entity)); + AssertResult.update(i, 1); + + Long id = entity.getId().orElseThrow(); + this.roleMenuRefDAO.clearRoleMenuRefs(entity); + this.roleMenuRefDAO.saveRoleMenuRefs(id, entity); + this.rolePermissionRefDAO.clearRolePermissionRefs(entity); + this.rolePermissionRefDAO.saveRolePermissionRefs(id, entity); + return entity; + } + + @Override + public Collection findByAccountId(Long accountId) { + return queryForList(""" + SELECT r."id", r."name", r."identifier", r."status", r."remarks", r."version" + FROM sys_role AS r + LEFT JOIN sys_account_role AS ar ON r.id = ar.role_id + WHERE ar.account_id = :accountId AND r.deleted = 0 + """, new MapSqlParameterSource("accountId", accountId)); + } + + @Override + protected final Role mapRow(ResultSet rs) throws SQLException { + long roleId = rs.getLong("id"); + return new Role( + roleId, + rs.getString("name"), + rs.getString("identifier"), + EntityStatus.of(rs.getInt("status")), + rs.getString("remarks"), + this.roleMenuRefDAO.selectMenuRefsByRoleId(roleId), + this.rolePermissionRefDAO.selectPermissionRefsByRoleId(roleId), + rs.getLong("version")); + } + + @Override + protected final MapSqlParameterSource generateParamSource(Long id, @Nonnull Role entity) { + LocalDateTime now = LocalDateTime.now(); + long loginId = adminAuthLogic.getLoginIdAsLong(); + return new MapSqlParameterSource() + .addValue("id", id) + .addValue("name", entity.getName()) + .addValue("identifier", entity.getIdentifier()) + .addValue("status", entity.getStatus().getValue()) + .addValue("remarks", entity.getRemarks()) + .addValue("createTime", now) + .addValue("createdBy", loginId) + .addValue("updateTime", now) + .addValue("updatedBy", loginId) + .addValue("version", entity.getVersion()); + } +} diff --git a/plusone-system/pom.xml b/plusone-system/pom.xml new file mode 100644 index 0000000..ff51ce4 --- /dev/null +++ b/plusone-system/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + plusone + xyz.zhouxy + 1.0.0-SNAPSHOT + + + xyz.zhouxy + plusone-system + 1.0.0-SNAPSHOT + pom + + plusone-system + + + plusone-system-common + plusone-system-domain + plusone-system-infrastructure + plusone-system-application + + + + + + xyz.zhouxy + plusone-basic-common + ${plusone-basic.version} + + + xyz.zhouxy + plusone-basic-domain + ${plusone-basic.version} + + + xyz.zhouxy + plusone-basic-infrastructure + ${plusone-basic.version} + + + xyz.zhouxy + plusone-basic-application + ${plusone-basic.version} + + + + xyz.zhouxy + plusone-system-common + ${plusone-system.version} + + + xyz.zhouxy + plusone-system-domain + ${plusone-system.version} + + + xyz.zhouxy + plusone-system-infrastructure + ${plusone-system.version} + + + xyz.zhouxy + plusone-system-application + ${plusone-system.version} + + + + + + + + org.junit.jupiter + junit-jupiter-api + test + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f2702f3 --- /dev/null +++ b/pom.xml @@ -0,0 +1,211 @@ + + + + 4.0.0 + + xyz.zhouxy + plusone + 1.0.0-SNAPSHOT + pom + + plusone + + http://zhouxy.xyz + + + plusone-basic + plusone-system + plusone-start + + + + UTF-8 + 17 + 17 + 17 + + 2.7.6 + 1.32.0 + 5.8.9 + 2.2.2 + 3.5.2 + 2.11.0 + 2.7.5 + 1.5.3.Final + 30.1-jre + 3.12.0 + 4.4 + 3.6.1 + 2.11.1 + 1.12.0 + 3.1.622 + + 1.0.0-SNAPSHOT + 1.0.0-SNAPSHOT + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + cn.dev33 + sa-token-core + ${sa-token.version} + + + + cn.dev33 + sa-token-spring-boot-starter + ${sa-token.version} + + + + cn.dev33 + sa-token-dao-redis-jackson + ${sa-token.version} + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + ${mybatis-starter.version} + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + + cn.hutool + hutool-all + ${hutool.version} + + + cn.hutool + hutool-core + ${hutool.version} + + + cn.hutool + hutool-crypto + ${hutool.version} + + + cn.hutool + hutool-captcha + ${hutool.version} + + + commons-io + commons-io + ${commons-io.version} + + + com.tencentcloudapi + tencentcloud-sdk-java + ${tencentcloud-sdk.version} + + + com.tencentcloudapi + tencentcloud-sdk-java-sms + ${tencentcloud-sdk.version} + + + + net.dreamlu + mica-xss + ${mica.version} + + + + com.squareup.okio + okio + ${okio.version} + + + + com.google.guava + guava + ${guava.version} + + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + org.apache.commons + commons-collections4 + ${commons-collections4.version} + + + org.apache.commons + commons-math3 + ${commons-math3.version} + + + org.apache.commons + commons-pool2 + ${commons-pool2.version} + + + + + + + ch.qos.logback + logback-classic + + + com.google.code.findbugs + jsr305 + 3.0.2 + + + + + + aliyun + https://maven.aliyun.com/repository/public + + true + + + false + + + + + + aliyun-plugin + https://maven.aliyun.com/repository/public + + true + + + false + + + +